"""Golden wire behavior captured before the tooling/structure migration. Fixtures include permissive payloads and existing failures deliberately. Changing them requires a separate, explicitly approved API behavior change. """ from __future__ import annotations import asyncio import importlib import json import os from pathlib import Path import sys import unittest from unittest.mock import AsyncMock, patch import httpx from fastapi.testclient import TestClient sys.path.insert(0, str(Path(__file__).resolve().parents[1])) with patch.dict(os.environ, {}, clear=True), patch("dotenv.load_dotenv", return_value=False): server = importlib.import_module("server") ASYNC_CLIENT = httpx.AsyncClient FIXTURE = Path(__file__).parent / "fixtures" / "legacy-contracts.json" BASE_CONFIG = { "LOAD_BALANCER_URL": "", "SPEECH_TO_SPEECH_URL": "", "S2S_PROXY_TARGET": "", "S2S_PRIMARY_PROMPT": "", "SERPER_KEY": "", "S2S_REASONING_TYPE": "PROXY", "S2S_INJECT_PROFILE": False, "S2S_INJECT_REPORTS": False, "S2S_DEBUG_PROMPT": False, "S2S_START_SESSION_MODE": "", "S2S_THEME": "", "VOCERO_USER_PHONE_AUTH_URL": "", "S2S_REPORT_LIST": "", "S2S_LOGIN_BY_PHONE": "", "S2S_LATEST_SUMMARY": "", "S2S_LATEST_SUMMARY_TEXT": "", "S2S_APIPILOT_TOOLS_URL": "", "S2S_APIPILOT_TOOLS_EXECUTE_URL": "", "S2S_APIPILOT_SERVICE_TOKEN": "", "S2S_APIPILOT_API_KEY": "", "OPENAI_API_KEY": "", "NODE_RED_REASONING_URL": "", "S2S_REPORTS_QUICK_HINT_MAX": 5, "S2S_REPORTS_INJECT_MODE": "minimal", } TOOL = { "name": "fixture_tool", "description": "A fixture tool.", "parameters": {"type": "object", "properties": {"count": {"type": "integer"}}, "required": ["count"]}, "requires_auth": False, "future_definition_field": {"nested": True}, } def cases(): """Each case controls all upstream IO; no deployment services are contacted.""" yield "config-default", "GET", "/api/config", {}, {}, [] yield "config-proxy", "GET", "/api/config", { "headers": {"x-forwarded-proto": "https", "x-forwarded-host": "voice.test, internal.test"}, }, {"S2S_PROXY_TARGET": "ws://private.test/voice"}, [] yield "config-direct", "GET", "/api/config", {}, {"SPEECH_TO_SPEECH_URL": "wss://voice.test/realtime?token=fixture"}, [] yield "config-lb", "GET", "/api/config", {}, {"LOAD_BALANCER_URL": "http://lb.test"}, [] for path in ("/api/me", "/api/session/heartbeat", "/api/session/end", "/", "/server.py"): yield f"removed-{path}", "GET", path, {}, {}, [] for path in ("/project", "/project/", "/project/index.html", "/public/docs.html"): yield f"redirect-{path}", "GET", path, {}, {}, [] for path in ("/api/session", "/api/queue/end"): yield f"disabled-{path}", "POST", path, {}, {}, [] for method in ("GET", "DELETE"): yield f"disabled-queue-{method}", method, "/api/queue/ticket", {}, {}, [] for name, payload in ( ("missing", {}), ("blank", {"query": " "}), ("wrong-type", {"query": 17}), ("null", {"query": None}), ("unconfigured", {"query": "hello"}), ): yield f"search-{name}", "POST", "/api/search", {"json": payload}, {}, [] search_config = {"SERPER_KEY": "server-fixture-key"} yield "search-success-extra-fields", "POST", "/api/search", {"json": { "query": " hello ", "key": "caller-fixture-key", "unknown_request_field": [1, 2], }}, search_config, [(200, {"answerBox": {"answer": "Answer"}, "organic": [ {"title": "Title", "snippet": "Text", "link": "https://example.test", "unknown": 1}, ], "future_upstream": {"nested": True}})] yield "search-loose-values", "POST", "/api/search", {"json": {"query": "hello"}}, search_config, [(200, { "answerBox": {"answer": {"future": [1, None]}}, "organic": [{"title": None, "snippet": 3, "link": {"href": "unusual"}}, {}], })] yield "search-upstream-error", "POST", "/api/search", {"json": {"query": "hello"}}, search_config, [(429, {"message": "Fixture credits exhausted"})] yield "search-invalid-upstream-json", "POST", "/api/search", {"json": {"query": "hello"}}, search_config, [(200, b"not-json")] yield "profile-missing-phone", "GET", "/api/user/phone", {}, {}, [] yield "profile-unconfigured", "GET", "/api/user/phone?phoneNumber=123", {}, {}, [] profile_config = {"VOCERO_USER_PHONE_AUTH_URL": "http://profile.test/user"} yield "profile-blank-phone", "GET", "/api/user/phone?phoneNumber=%20", {}, profile_config, [] for name, status, payload in ( ("object", 200, {"result": {"name": "Caller", "unknown": {"nested": [1, None]}}}), ("array", 200, [1, {"future": "field"}]), ("null", 200, None), ("string", 200, "opaque profile"), ("error", 403, {"future_error": True}), ("invalid-json", 200, b"not-json"), ): yield f"profile-{name}", "GET", "/api/user/phone?phoneNumber=%2B56000000000", {}, profile_config, [(status, payload)] yield "profile-login-token", "GET", "/api/user/phone?phoneNumber=123", {}, { **profile_config, "S2S_LOGIN_BY_PHONE": "http://profile.test/login", }, [(200, {"result": {"data": {"accessToken": "fixture-token", "future": True}}}), (200, {"name": "Caller"})] yield "reports-disabled", "GET", "/api/user/reports", {}, {}, [] yield "reports-unconfigured", "GET", "/api/user/reports", {}, {"S2S_INJECT_REPORTS": True}, [] reports_config = {"S2S_INJECT_REPORTS": True, "S2S_REPORT_LIST": "http://reports.test/list"} yield "reports-empty", "GET", "/api/user/reports", {}, reports_config, [(200, [])] yield "reports-upstream-failure", "GET", "/api/user/reports", {}, reports_config, [(503, {"error": "fixture"})] yield "reports-normalization", "GET", "/api/user/reports?phoneNumber=123", {}, reports_config, [(200, {"result": [ {"sourceScheduleLabel": "Daily", "date": "2026-01-02T12:00:00Z", "podcastScript": "private script", "future": True}, {"sourceScheduleLabel": "Daily", "date": "2026-01-01T12:00:00Z", "podcastScript": "older script"}, {"sourceSchedule": {"name": "Weekly", "date": "2026-01-03T12:00:00Z"}, "text": "weekly script"}, ], "future": True})] lb_config = {"LOAD_BALANCER_URL": "http://lb.test"} for name, status, payload in ( ("grant", 200, {"session_id": "session", "connect_url": "ws://voice.test?token=fixture", "future": {"nested": True}}), ("camelcase", 200, {"sessionId": "session", "connectUrl": "ws://voice.test", "unknown": None}), ("queued", 200, {"state": "queued", "queue_id": "ticket", "position": 3, "future": [1]}), ("empty", 200, {}), ("capacity", 503, {"state": "at_capacity", "future": True}), ("other-error", 429, {"error": "fixture"}), ("invalid-json", 200, b"not-json"), ): yield f"session-{name}", "POST", "/api/session", {"content": b"arbitrary ignored request body"}, lb_config, [(status, payload)] for name, status, payload in ( ("waiting", 200, {"state": "queued", "queue_id": "ticket", "position": None, "future": True}), ("grant", 200, {"session_id": "session", "connect_url": "ws://voice.test"}), ("expired", 404, {"unknown": True}), ("error", 500, {}), ("invalid-json", 200, b"bad"), ): yield f"queue-{name}", "GET", "/api/queue/ticket", {}, lb_config, [(status, payload)] yield "queue-delete", "DELETE", "/api/queue/ticket", {}, lb_config, [(500, {})] for name, kwargs, upstream in ( ("valid", {"json": {"queueId": "ticket", "future": True}}, [(204, b"")]), ("empty", {"content": b""}, []), ("invalid-json", {"content": b"bad"}, []), ("array", {"json": ["ticket"]}, []), ("null-id", {"json": {"queueId": None}}, []), ("numeric-id", {"json": {"queueId": 42}}, []), ): yield f"queue-end-{name}", "POST", "/api/queue/end", kwargs, lb_config, upstream for name, payload in (("missing", {}), ("proxy", {"transcript": "hello"}), ("proxy-blank", {"transcript": " "})): yield f"reasoning-{name}", "POST", "/api/reasoning", {"json": payload}, {}, [] for mode in ("OPENAI", "NODE_RED"): yield f"reasoning-{mode}-legacy-unbound-token", "POST", "/api/reasoning", {"json": {"transcript": "hello"}}, {"S2S_REASONING_TYPE": mode}, [] yield "reasoning-legacy-unbound-profile", "POST", "/api/reasoning", {"json": {"transcript": "hello"}}, { "S2S_REASONING_TYPE": "OPENAI", "S2S_INJECT_PROFILE": True, "S2S_LATEST_SUMMARY_TEXT": "fixture summary", }, [] reasoning_config = {**profile_config, "S2S_INJECT_PROFILE": True, "S2S_REASONING_TYPE": "NODE_RED", "NODE_RED_REASONING_URL": "http://reasoning.test"} reasoning_request = {"json": {"transcript": " hello ", "phoneNumber": "123", "instructions": "Fixture instructions", "language": "es", "future": True}} for name, payload in ( ("answer", {"answer": " answer ", "latency_ms": "loose", "intent": {"future": 1}, "usage": {"future": True}, "future": True}), ("log-only", {"answer": "", "log_only": True, "debug": {"future": True}}), ("invalid-answer", {"answer": 5}), ): yield f"reasoning-node-{name}", "POST", "/api/reasoning", reasoning_request, reasoning_config, [(200, {"name": "Caller"}), (200, payload)] yield "reasoning-openai-answer", "POST", "/api/reasoning", reasoning_request, { **reasoning_config, "S2S_REASONING_TYPE": "OPENAI", "OPENAI_API_KEY": "fixture-key", }, [(200, {"name": "Caller"}), (200, {"choices": [{"message": {"content": " answer ", "future": True}}], "usage": {"total_tokens": 12}, "future": True})] for method, path, kwargs in (("GET", "/api/apipilot/tools", {}), ("POST", "/api/apipilot/execute", {"json": {"name": "fixture_tool"}})): yield f"apipilot-disabled-{method}", method, path, kwargs, {}, [] tool_config = {"S2S_APIPILOT_TOOLS_URL": "http://tools.test/apipilot"} yield "tools-query-validation", "GET", "/api/apipilot/tools?refresh=invalid", {}, tool_config, [] for name, payload in (("list", [TOOL]), ("result", {"result": [TOOL]}), ("envelope", {"data": {"tools": [TOOL]}, "future": True}), ("opaque", "anything")): yield f"tools-{name}", "GET", "/api/apipilot/tools?include_unauthorized=false&refresh=true", {"headers": {"Authorization": "Bearer fixture-token", "x-demo-session": "fixture-session"}}, tool_config, [(200, payload)] yield "tools-upstream-error", "GET", "/api/apipilot/tools", {}, tool_config, [(403, {"error": "fixture"})] yield "execute-discovery-error-envelope", "POST", "/api/apipilot/execute", {"json": {"name": "fixture_tool"}}, tool_config, [(503, {})] yield "execute-unknown-tool", "POST", "/api/apipilot/execute", {"json": {"name": "unknown"}}, tool_config, [(200, [TOOL]), (200, [TOOL])] yield "execute-missing-argument", "POST", "/api/apipilot/execute", {"json": {"name": "fixture_tool"}}, tool_config, [(200, [TOOL])] yield "execute-api-key-precheck", "POST", "/api/apipilot/execute", {"json": {"name": "fixture_tool", "arguments": {"count": 1}}}, { **tool_config, "S2S_APIPILOT_API_KEY": "fixture-key", }, [(200, [{**TOOL, "requires_auth": True}])] for name, payload in ( ("envelope", {"success": True, "data": {"future": [1, None]}, "error": None, "future_envelope": True}), ("raw-list", [{"future": True}]), ("minimal-envelope", {"success": True}), ("error", {"success": False, "data": None, "error": {"status_code": 409, "code": "FIXTURE", "message": "Fixture", "future": True}}), ): yield f"execute-{name}", "POST", "/api/apipilot/execute", {"json": {"name": "fixture_tool", "arguments": {"count": "wrong-type-is-accepted", "unknown": 1}, "future": True}}, tool_config, [(200, [TOOL]), (200, payload)] def capture_http(): captures = {} for name, method, path, kwargs, config, responses in cases(): remaining = iter(responses) requests = [] def upstream(request): requests.append({ "method": request.method, "url": str(request.url), "body": request.content.decode(), "headers": {key: request.headers[key] for key in ( "authorization", "x-demo-session", "x-api-key", "content-type", "x-s2s-caller", ) if key in request.headers}, }) status, payload = next(remaining) if isinstance(payload, bytes): return httpx.Response(status, content=payload) return httpx.Response(status, content=json.dumps(payload).encode(), headers={"content-type": "application/json"}) with ( patch.multiple(server, **{**BASE_CONFIG, **config}), patch.object(server, "_APIPILOT_CACHE", {}), patch.object(server, "_build_today_fixed_context_block", return_value="Fixture current date: 2026-01-04"), patch.object(server.httpx, "AsyncClient", side_effect=lambda **opts: ASYNC_CLIENT(transport=httpx.MockTransport(upstream), **opts)), TestClient(server.app, raise_server_exceptions=False) as client, ): response = client.request(method, path, follow_redirects=False, **kwargs) captures[name] = { "status": response.status_code, "body": response.text, "headers": {key: response.headers[key] for key in ("content-type", "location", "set-cookie") if key in response.headers}, "upstream": requests, } if list(remaining): raise AssertionError(f"Unused upstream fixtures in {name}") return captures def capture_openapi_requests(): schema = server.app.openapi() return { "operations": { f"{method.upper()} {path}": {key: operation[key] for key in ("operationId", "parameters", "requestBody") if key in operation} for path, methods in schema["paths"].items() for method, operation in methods.items() }, "models": {name: schema["components"]["schemas"][name] for name in ( "SearchRequest", "ReasoningRequest", "ApipilotExecuteRequest", "HTTPValidationError", "ValidationError", )}, } async def capture_realtime(): frames = [] class Socket: async def send_text(self, message): frames.append(message) async def send_bytes(self, message): frames.append({"binary": message.hex()}) async def upstream(): yield b"\x00\x01\x02\x03" yield json.dumps({"type": "future.event", "future": {"nested": [None, 1]}}) yield json.dumps({"type": "response.text.done", "text": "Hello [rightspeak]world[endspeak]", "future": True}) yield json.dumps({"type": "error", "error": {"message": "Unknown or invalid event: session.update"}}) yield json.dumps({"type": "future.after.suppressed.error"}) with patch.object(server, "_forward_agent_text_to_nodered", new_callable=AsyncMock): await server._upstream_to_client(Socket(), upstream(), "fixture", {}) return frames def capture(): return {"http": capture_http(), "openapi_requests": capture_openapi_requests(), "realtime": asyncio.run(capture_realtime())} class LegacyCompatibilityTests(unittest.TestCase): def test_wire_contracts_match_pre_migration_capture(self): expected = json.loads(FIXTURE.read_text()) actual = capture() for name, response in actual["http"].items(): with self.subTest(case=name): self.assertEqual(response, expected["http"][name]) self.assertEqual(actual["openapi_requests"], expected["openapi_requests"]) self.assertEqual(actual["realtime"], expected["realtime"]) self.assertEqual(set(actual["http"]), set(expected["http"]))