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
@@ -0,0 +1,150 @@
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import type { ClientOptions } from "./client";
import { VoiceController } from "./controller";
const { MockClient, closeContext } = vi.hoisted(() => {
const closeContext = vi.fn().mockResolvedValue(undefined);
class MockClient {
static instances: MockClient[] = [];
readonly options: ClientOptions;
private listeners = new Map<string, Set<(value: unknown) => void>>();
constructor(options: ClientOptions) {
this.options = options;
MockClient.instances.push(this);
}
on(name: string, listener: (value: unknown) => void) {
const listeners = this.listeners.get(name) ?? new Set();
listeners.add(listener);
this.listeners.set(name, listeners);
return () => {
listeners.delete(listener);
};
}
emit(name: string, value: unknown) {
this.listeners.get(name)?.forEach((listener) => {
listener(value);
});
}
connect = vi.fn(() => {
this.emit("status", "listening");
return Promise.resolve();
});
close = vi.fn(() => {
this.emit("status", "closed");
return Promise.resolve();
});
updateSession = vi.fn();
setGate = vi.fn();
setMuted = vi.fn();
renderExternalAnswer = vi.fn();
sendToolOutput = vi.fn();
join = vi.fn();
}
return { MockClient, closeContext };
});
vi.mock("./client", async (importOriginal) => ({ ...(await importOriginal()), RealtimeClient: MockClient }));
vi.mock("../audio/engine", () => ({ createAudioContext: () => ({ state: "running", close: closeContext }) }));
let pendingReasoning: ((value: Response) => void) | undefined;
let controller: VoiceController;
let fetchMock: Mock<(path: string, init?: RequestInit) => Promise<Response>>;
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
MockClient.instances = [];
pendingReasoning = undefined;
vi.stubGlobal("AudioContext", vi.fn());
vi.stubGlobal("navigator", { mediaDevices: { getUserMedia: vi.fn() } });
fetchMock = vi.fn((path: string) => {
if (path === "/api/reasoning") {
return new Promise<Response>((resolve) => {
pendingReasoning = resolve;
});
}
const pathname = new URL(path, "http://localhost").pathname;
const responses: Record<string, unknown> = {
"/api/config": { reasoningType: "OPENAI", injectProfile: false, s2sUrl: "ws://localhost/v1/realtime" },
"/api/user/phone": { result: { name: "Ana" } },
"/api/user/reports": { count: 0 },
};
return Promise.resolve(new Response(JSON.stringify(responses[pathname] ?? {})));
});
vi.stubGlobal("fetch", fetchMock);
controller = new VoiceController({ phoneNumber: "1234", episodeId: "episode1" });
});
afterEach(async () => {
await controller.stop();
vi.useRealTimers();
vi.unstubAllGlobals();
});
async function start() {
await controller.initialize(new AbortController().signal);
await controller.start();
const client = MockClient.instances[0];
if (!client) {
throw new Error("No client created");
}
return client;
}
describe("voice application lifecycle", () => {
it("loads configuration and caller context without an account request on startup or stop", async () => {
const client = await start();
await controller.stop();
expect(client.close).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls.map(([path]) => new URL(path, "http://localhost").pathname)).toEqual([
"/api/config",
"/api/user/phone",
"/api/user/reports",
]);
});
it("renders an external answer and preserves separate profile context", async () => {
const client = await start();
client.emit("transcript", { id: "u1", role: "user", text: "Hola", partial: false });
pendingReasoning?.(new Response(JSON.stringify({ answer: " Hola, Ana " })));
await vi.advanceTimersByTimeAsync(1);
const request = fetchMock.mock.calls.find(([path]) => path === "/api/reasoning");
expect(request?.[1]?.body).toContain('"phoneNumber":"1234"');
expect(request?.[1]?.body).toContain("Caller profile resolved");
expect(client.options.instructions).not.toContain("Caller profile resolved");
expect(client.renderExternalAnswer).toHaveBeenCalledWith("Hola, Ana");
expect(controller.getSnapshot().messages.map((message) => message.text)).toEqual(["Hola", "Hola, Ana"]);
});
it.each(["PROXY", "UNSUPPORTED"])("keeps native reasoning for mode %s", async (reasoningType) => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ reasoningType, s2sUrl: "ws://localhost/v1/realtime" })),
);
const client = await start();
client.emit("transcript", { id: "u1", role: "user", text: "Hola", partial: false });
await vi.advanceTimersByTimeAsync(1);
expect(fetchMock.mock.calls.some(([path]) => path === "/api/reasoning")).toBe(false);
expect(client.renderExternalAnswer).not.toHaveBeenCalled();
expect(controller.getSnapshot().messages.map((message) => message.text)).toEqual(["Hola"]);
});
it("discards external reasoning that finishes after the session was stopped", async () => {
const client = await start();
client.emit("transcript", { id: "u1", role: "user", text: "Consulta", partial: false });
await controller.stop();
pendingReasoning?.(new Response(JSON.stringify({ answer: "Respuesta tardía" })));
await vi.advanceTimersByTimeAsync(1);
expect(client.renderExternalAnswer).not.toHaveBeenCalled();
expect(controller.getSnapshot().messages.map((message) => message.text)).toEqual(["Consulta"]);
});
it("applies and removes RAG context in the live session", async () => {
const client = await start();
controller.setRag("Datos importantes");
expect(client.updateSession).toHaveBeenLastCalledWith(expect.stringContaining("Datos importantes"), "Aiden", []);
controller.setRag("");
expect(client.updateSession).toHaveBeenLastCalledWith(expect.not.stringContaining("CUSTOM_RAG_BEGIN"), "Aiden", []);
});
it("closes after silence but does not cut off the assistant while speaking", async () => {
const client = await start();
client.emit("status", "ai-speaking");
await vi.advanceTimersByTimeAsync(60000);
expect(client.close).not.toHaveBeenCalled();
client.emit("status", "listening");
await vi.advanceTimersByTimeAsync(30000);
expect(client.close).toHaveBeenCalledOnce();
expect(controller.getSnapshot().status).toBe("closed");
});
});