refactor(server): adopt uv and modular typed FastAPI structure

This commit is contained in:
2026-10-06 18:18:36 -03:00
parent 34f0d1b125
commit 92c53dc7ca
38 changed files with 8363 additions and 6622 deletions
+559
View File
@@ -0,0 +1,559 @@
"""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 json
import os
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, patch
import httpx
from fastapi.testclient import TestClient
with patch.dict(os.environ, {}, clear=True), patch("dotenv.load_dotenv", return_value=False):
from s2s_client import main as server
from s2s_client.core import config as settings
from s2s_client.providers import apipilot
from s2s_client.realtime import proxy
from s2s_client.services import context, reasoning
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=requests, remaining=remaining):
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(settings, **{**BASE_CONFIG, **config}),
patch.object(apipilot, "_APIPILOT_CACHE", {}),
patch.object(context, "_build_today_fixed_context_block", return_value="Fixture current date: 2026-01-04"),
patch.object(
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(reasoning, "_forward_agent_text_to_nodered", new_callable=AsyncMock):
await proxy._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"]))