"""Server endpoint tests. API 02 adds POST /reset endpoint tests. API 04 adds a smoke test for GET /scenarios. API 13 adds CORS middleware verification tests. API 03 adds POST /step endpoint tests. API 06 adds WebSocket session handler tests. API 07 adds idle-timeout and graceful disconnect cleanup tests. """ from __future__ import annotations import json import time from unittest.mock import patch import pytest from fastapi.testclient import TestClient from starlette.websockets import WebSocketDisconnect from replicalab.models import ScientistAction from server.app import app _EXPECTED_FAMILIES = {"math_reasoning", "ml_benchmark", "finance_trading"} _EXPECTED_DIFFICULTIES = ["easy", "medium", "hard"] @pytest.fixture() def client(): return TestClient(app) class TestHealthEndpoint: """GET /health — API 01.""" def test_health_returns_200(self, client: TestClient) -> None: resp = client.get("/health") assert resp.status_code == 200 def test_health_payload_has_stable_keys(self, client: TestClient) -> None: data = client.get("/health").json() assert data["status"] == "ok" assert data["env"] in ("real", "stub") assert "version" in data def test_health_version_matches_app(self, client: TestClient) -> None: from server.app import app as _app data = client.get("/health").json() assert data["version"] == _app.version def test_health_is_deterministic(self, client: TestClient) -> None: r1 = client.get("/health").json() r2 = client.get("/health").json() assert r1 == r2 class TestRuntimeEndpoint: """Local runtime metadata for model-backed Scientist stepping.""" def test_runtime_defaults_to_baseline_without_api_key( self, client: TestClient, monkeypatch ) -> None: monkeypatch.delenv("REPLICALAB_SCIENTIST_RUNTIME", raising=False) monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) resp = client.get("/runtime") assert resp.status_code == 200 data = resp.json() assert data["scientist_runtime"] == "baseline" assert data["scientist_model"] == "baseline-heuristic" assert data["agent_step_available"] is True def test_runtime_reports_anthropic_when_enabled( self, client: TestClient, monkeypatch ) -> None: monkeypatch.setenv("REPLICALAB_SCIENTIST_RUNTIME", "anthropic") monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") resp = client.get("/runtime") assert resp.status_code == 200 data = resp.json() assert data["scientist_runtime"] == "anthropic" assert data["scientist_ready"] is True assert data["agent_step_available"] is True class TestLogConfig: """OBS 02 — log level configurability.""" def test_default_log_level_is_info(self) -> None: from replicalab.config import LOG_LEVEL # Default when REPLICALAB_LOG_LEVEL is not set assert LOG_LEVEL in ("INFO", "DEBUG", "WARNING", "ERROR") def test_log_level_env_var_is_respected(self, monkeypatch) -> None: """REPLICALAB_LOG_LEVEL env var controls the log level.""" import importlib import replicalab.config as config_mod monkeypatch.setenv("REPLICALAB_LOG_LEVEL", "debug") importlib.reload(config_mod) assert config_mod.LOG_LEVEL == "DEBUG" # Restore monkeypatch.delenv("REPLICALAB_LOG_LEVEL", raising=False) importlib.reload(config_mod) def test_log_format_is_readable(self) -> None: from replicalab.config import LOG_FORMAT assert "%(asctime)s" in LOG_FORMAT assert "%(levelname)s" in LOG_FORMAT assert "%(name)s" in LOG_FORMAT class TestRootEndpoint: """GET / — lightweight landing page for hosted backend deployments.""" def test_root_returns_200_html(self, client: TestClient) -> None: resp = client.get("/") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] def test_root_mentions_core_api_endpoints(self, client: TestClient) -> None: body = client.get("/").text # When frontend/dist exists, root serves the SPA; otherwise the API landing assert "ReplicaLab" in body if "ReplicaLab API" in body: assert "GET /health" in body assert "POST /reset" in body class TestWebFallback: """GET /web — API 19: OpenEnv fallback UI.""" def test_web_returns_200_html(self, client: TestClient) -> None: resp = client.get("/web") assert resp.status_code == 200 assert "text/html" in resp.headers["content-type"] def test_web_contains_interactive_controls(self, client: TestClient) -> None: body = client.get("/web").text assert "ReplicaLab" in body assert "btnReset" in body assert "btnPropose" in body assert "btnAccept" in body assert "/reset" in body assert "/step" in body def test_web_is_self_contained(self, client: TestClient) -> None: """Fallback UI must work without external JS/CSS dependencies.""" body = client.get("/web").text assert "