refactor: rename API and frontend projects to Vocero

Rename the project directories, Python distribution and import package, and npm package. Update launcher, Docker, tooling, tests, lockfiles, and documentation while preserving existing API contracts.

Make AGENTS.md self-contained and remove external skill and workflow references.
This commit is contained in:
2026-10-06 19:26:55 -03:00
parent e0304b2bdb
commit abd03ed07d
105 changed files with 353 additions and 312 deletions
+573
View File
@@ -0,0 +1,573 @@
"""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 patch
import httpx
from fastapi.testclient import TestClient
with patch.dict(os.environ, {}, clear=True), patch("dotenv.load_dotenv", return_value=False):
from vocero_api import main as server
from vocero_api.core import config as settings
from vocero_api.providers import apipilot
from vocero_api.realtime import proxy
from vocero_api.services import context
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": "",
"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}, {}, []
yield (
"reasoning-OPENAI-legacy-unbound-token",
"POST",
"/api/reasoning",
{"json": {"transcript": "hello"}},
{"S2S_REASONING_TYPE": "OPENAI"},
[],
)
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": "OPENAI",
}
reasoning_request = {
"json": {
"transcript": " hello ",
"phoneNumber": "123",
"instructions": "Fixture instructions",
"language": "es",
"future": True,
}
}
yield (
"reasoning-openai-answer",
"POST",
"/api/reasoning",
reasoning_request,
{
**reasoning_config,
"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",
)
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"})
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_native_transcripts_are_relayed_without_external_http_calls(self):
events = [
{"type": "conversation.item.input_audio_transcription.completed", "transcript": "Hola", "item_id": "u1"},
{"type": "response.audio_transcript.done", "transcript": "Respuesta", "response_id": "r1"},
{"type": "response.output_audio_transcript.done", "transcript": "Respuesta", "response_id": "r1"},
{"type": "response.content_part.done", "part": {"type": "text", "text": "Respuesta"}},
*[
{
"type": event_type,
"item": {"id": "a1", "role": "assistant", "content": [{"type": "text", "text": "Respuesta"}]},
}
for event_type in ("response.output_item.done", "conversation.item.created")
],
{"type": "response.text.done", "text": "Respuesta", "response_id": "r1"},
]
messages = [json.dumps(event) for event in events]
forwarded = []
class Socket:
async def send_text(self, message):
forwarded.append(message)
async def upstream():
for message in messages:
yield message
async def relay():
await proxy._upstream_to_client(Socket(), upstream(), "fixture", {})
# Allow any accidentally scheduled background HTTP work to run.
await asyncio.sleep(0)
for mode in ("PROXY", "OPENAI"):
forwarded.clear()
with (
self.subTest(mode=mode),
patch.multiple(settings, **{**BASE_CONFIG, "S2S_REASONING_TYPE": mode}),
patch.object(httpx, "AsyncClient", side_effect=AssertionError("Unexpected HTTP call")) as http,
):
asyncio.run(relay())
self.assertEqual(forwarded, messages)
http.assert_not_called()
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"]))