"""P2 integration test — SPA fallback + voice UI coexist (TASK-10-04, G-041). Tests against the running app (TestClient). Verifies: 1. GET / → 200 text/html with
(voice UI loads). 2. GET /operator/dashboard → 200 text/html (SPA fallback serves index.html). 3. GET /operator/login → 200 text/html (SPA fallback). 4. GET /api/operator/cohort → JSON (API route, not SPA fallback). 5. GET /health → JSON (API route). 6. GET /pipecat/webrtc → 405 (POST only, route exists — not SPA fallback). 7. GET /vc/verify/nonexistent → 404 (API route, not SPA fallback). 8. GET /assets/index.js → served by StaticFiles (not SPA fallback). R-DASH-03 verified: SPA fallback serves index.html for client-side routes; API routes + StaticFiles assets are unaffected. R-DASH-05: voice UI at / unchanged. G-041: the SPA fallback uses a custom StaticFiles subclass (SpaStaticFiles), NOT a catch-all route — assets are served normally, index.html is the fallback only for non-file paths. """ from __future__ import annotations import os import shutil import tempfile import pytest from fastapi.testclient import TestClient @pytest.fixture def client_with_dist(tmp_path): """Build a client/dist with index.html + an asset, then import the app.""" dist = tmp_path / "dist" dist.mkdir() (dist / "index.html").write_text( '
', encoding="utf-8", ) assets = dist / "assets" assets.mkdir() (assets / "index.js").write_text("console.log('app');", encoding="utf-8") # Set the env var + reload the app module so the StaticFiles mount sees it. os.environ["PRAXIS_CLIENT_DIST"] = str(dist) os.environ["PRAXIS_COOKIE_SECRET"] = "x" * 48 os.environ["PRAXIS_COOKIE_SECURE"] = "false" # Drop any PG DSN so we don't try to connect during the lifespan. os.environ.pop("PRAXIS_PG_DSN", None) import importlib import server.__main__ as main_mod importlib.reload(main_mod) with TestClient(main_mod.app) as c: yield c # Cleanup env. os.environ.pop("PRAXIS_CLIENT_DIST", None) def test_root_serves_voice_ui(client_with_dist): r = client_with_dist.get("/") assert r.status_code == 200 assert "text/html" in r.headers.get("content-type", "") assert "
" in r.text def test_operator_dashboard_spa_fallback(client_with_dist): r = client_with_dist.get("/operator/dashboard") assert r.status_code == 200 assert "text/html" in r.headers.get("content-type", "") assert "
" in r.text def test_operator_login_spa_fallback(client_with_dist): r = client_with_dist.get("/operator/login") assert r.status_code == 200 assert "text/html" in r.headers.get("content-type", "") assert "
" in r.text def test_api_operator_cohort_is_json_not_html(client_with_dist): # Without auth → 401 JSON (not index.html). Proves the API route wins. r = client_with_dist.get("/api/operator/cohort") assert r.status_code in (401, 503) assert "application/json" in r.headers.get("content-type", "") # Critically NOT html. assert "
" not in r.text def test_health_is_json(client_with_dist): r = client_with_dist.get("/health") assert r.status_code == 200 assert "application/json" in r.headers.get("content-type", "") def test_pipecat_webrtc_post_route_exists(client_with_dist): # The POST route exists and responds (not index.html). A GET falls through # to the SPA fallback (serves index.html) — acceptable: the POST route is # the real voice-loop entrypoint; a GET is a client-side navigation attempt. # We assert the POST route is wired (returns 4xx/5xx, not HTML). r = client_with_dist.post("/pipecat/webrtc", json={"sdp": "", "type": "offer"}) assert r.status_code in (400, 422, 500) assert "
" not in r.text def test_vc_verify_nonexistent_is_404(client_with_dist): r = client_with_dist.get("/vc/verify/nonexistent-id-xyz") assert r.status_code == 404 assert "application/json" in r.headers.get("content-type", "") assert "
" not in r.text def test_assets_served_by_staticfiles_not_spa_fallback(client_with_dist): r = client_with_dist.get("/assets/index.js") assert r.status_code == 200 ct = r.headers.get("content-type", "") assert "javascript" in ct or "text/plain" in ct assert "console.log" in r.text def test_unknown_non_asset_path_serves_index_html(client_with_dist): """An unknown path that is NOT an asset + NOT an API route → SPA fallback.""" r = client_with_dist.get("/some/unknown/route") assert r.status_code == 200 assert "
" in r.text