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
+15
View File
@@ -0,0 +1,15 @@
import type { ProxyOptions } from "vite";
/** Preserve the public origin used by FastAPI to construct the speech WebSocket URL. */
export function createBackendProxy(target: string): Record<string, ProxyOptions> {
const http: ProxyOptions = { target, changeOrigin: false };
return {
"/api": http,
"/v1/realtime": { ...http, ws: true },
"/docs": http,
"/redoc": http,
"/openapi.json": http,
"/project": http,
"/public/docs.html": http,
};
}
@@ -0,0 +1,226 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test, type TestContext } from "node:test";
import { setTimeout as delay } from "node:timers/promises";
// Exercise the real Bash supervisor with lightweight children, without installing
// packages, starting upstream services, or reading the checkout's private .env.
const childSource = `
import { createServer } from "node:http";
import { writeFileSync } from "node:fs";
import { spawn } from "node:child_process";
const role = process.argv[2] === "backend" ? "backend" : "frontend";
const root = process.env.S2S_TEST_ROOT;
const args = process.argv.slice(2);
writeFileSync(root + "/" + role + ".pid", String(process.pid));
writeFileSync(root + "/" + role + ".json", JSON.stringify({
args, target: process.env.S2S_BACKEND_URL, hosts: process.env.S2S_DEV_ALLOWED_HOSTS,
nodeEnv: process.env.NODE_ENV,
}));
if (process.env.S2S_TEST_FAIL === role) process.exit(37);
const worker = spawn(process.execPath, ["-e",
'const fs = require("node:fs"); process.on("SIGTERM", () => { fs.writeFileSync(process.argv[1], "yes"); process.exit(0); }); fs.writeFileSync(process.argv[2], "yes"); setInterval(() => {}, 1000);',
root + "/" + role + "-worker-stopped", root + "/" + role + "-worker-ready",
], { stdio: "ignore" });
process.on("SIGTERM", () => {
worker.once("exit", () => process.exit(0));
if (worker.exitCode !== null || worker.signalCode !== null) process.exit(0);
});
createServer((request, response) => {
response.setHeader("Content-Type", "application/json");
response.end("{}");
}).listen(Number(args[args.indexOf("--port") + 1]), "127.0.0.1");
`;
async function freePort() {
const server = createServer();
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
assert.ok(address && typeof address === "object");
await new Promise<void>((resolve) =>
server.close(() => {
resolve();
}),
);
return address.port;
}
async function waitForFile(filename: string) {
for (let attempt = 0; attempt < 100; attempt++) {
try {
return await readFile(filename, "utf8");
} catch {
await delay(30);
}
}
assert.fail(`Timed out waiting for ${filename}`);
}
async function launcher(context: TestContext, env: Record<string, string> = {}) {
const root = await mkdtemp(join(tmpdir(), "s2s-launcher-"));
const frontend = join(root, "vocero-frontend");
const modules = join(frontend, "node_modules");
await mkdir(join(root, "vocero-api/.venv/bin"), { recursive: true });
await mkdir(join(root, "bin"));
await mkdir(join(modules, "vite/bin"), { recursive: true });
await copyFile(new URL("../../start_ui.sh", import.meta.url), join(root, "start_ui.sh"));
await writeFile(join(root, "vocero-api/pyproject.toml"), "");
await writeFile(join(root, "vocero-api/uv.lock"), "");
await writeFile(
join(root, "bin/uv"),
'#!/bin/bash\nprintf "%s\\n" "$@" > "$S2S_TEST_ROOT/uv-args"\nprintf "%s" "$UV_PROJECT_ENVIRONMENT" > "$S2S_TEST_ROOT/uv-environment"\nexit "${S2S_TEST_UV_EXIT:-0}"\n',
{ mode: 0o755 },
);
await writeFile(
join(root, "vocero-api/.venv/bin/python"),
'#!/bin/bash\nif [ "$1" = "-c" ]; then exit 0; fi\nexec node "$S2S_TEST_ROOT/child.mjs" backend "$@"\n',
{ mode: 0o755 },
);
await writeFile(join(root, "child.mjs"), childSource);
await writeFile(join(modules, "vite/bin/vite.js"), 'import(process.env.S2S_TEST_ROOT + "/child.mjs");\n');
for (const manifest of ["package.json", "package-lock.json"]) {
await writeFile(join(frontend, manifest), "{}\n");
await copyFile(join(frontend, manifest), join(modules, `.s2s-${manifest}`));
}
const publicPort = await freePort();
let backendPort = await freePort();
while (backendPort === publicPort) {
backendPort = await freePort();
}
// The root .env retains the launcher's existing precedence over shell values.
await writeFile(join(root, ".env"), `S2S_DEV_ALLOWED_HOSTS=s2s.test\nS2S_BACKEND_PORT=${backendPort}\n`);
const child = spawn("bash", [join(root, "start_ui.sh")], {
cwd: tmpdir(),
env: {
...process.env,
PATH: `${join(root, "bin")}:${process.env.PATH ?? ""}`,
UI_PORT: String(publicPort),
UI_HOST: "0.0.0.0",
NODE_ENV: "production",
S2S_BACKEND_PORT: "1",
S2S_BACKEND_URL: "http://wrong-target.invalid",
S2S_TEST_ROOT: root,
S2S_TEST_FAIL: "",
...env,
},
stdio: ["ignore", "pipe", "pipe"],
});
let output = "";
child.stdout.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
const exited = new Promise<number | null>((resolve) => child.once("close", resolve));
context.after(async () => {
child.kill("SIGTERM");
await exited;
await rm(root, { recursive: true, force: true });
});
return { root, child, exited, publicPort, backendPort, output: () => output };
}
for (const signal of ["SIGINT", "SIGTERM"] as const) {
await test(
`keeps the public endpoint and stops both process groups on ${signal}`,
{ timeout: 10_000 },
async (context) => {
const run = await launcher(context);
for (const role of ["backend", "frontend"]) {
await waitForFile(join(run.root, `${role}-worker-ready`));
}
const metadata: unknown = JSON.parse(await readFile(join(run.root, "frontend.json"), "utf8"));
assert.deepEqual(metadata, {
args: ["--host", "0.0.0.0", "--port", String(run.publicPort), "--strictPort"],
target: `http://127.0.0.1:${run.backendPort}`,
hosts: "s2s.test",
nodeEnv: "development",
});
assert.equal(run.output().includes("Installing"), false);
assert.equal(
await readFile(join(run.root, "uv-args"), "utf8"),
`sync\n--project\n${run.root}/vocero-api\n--locked\n`,
);
assert.equal(await readFile(join(run.root, "uv-environment"), "utf8"), `${run.root}/vocero-api/.venv`);
const backendMetadata: unknown = JSON.parse(await readFile(join(run.root, "backend.json"), "utf8"));
assert.deepEqual(backendMetadata, {
args: [
"backend",
"-m",
"uvicorn",
"vocero_api.main:app",
"--app-dir",
`${run.root}/vocero-api`,
"--host",
"127.0.0.1",
"--port",
String(run.backendPort),
"--reload",
"--reload-dir",
`${run.root}/vocero-api`,
],
target: `http://127.0.0.1:${run.backendPort}`,
hosts: "s2s.test",
nodeEnv: "production",
});
run.child.kill(signal);
assert.equal(await run.exited, signal === "SIGINT" ? 130 : 143);
for (const role of ["backend", "frontend"]) {
assert.equal(await waitForFile(join(run.root, `${role}-worker-stopped`)), "yes");
}
for (const port of [run.publicPort, run.backendPort]) {
await assert.rejects(fetch(`http://127.0.0.1:${port}`));
}
},
);
}
await test(
"stops the backend if Vite exits and returns the failing exit code",
{ timeout: 10_000 },
async (context) => {
const run = await launcher(context, { S2S_TEST_FAIL: "frontend" });
assert.equal(await run.exited, 37, run.output());
assert.equal(await waitForFile(join(run.root, "backend-worker-stopped")), "yes");
await assert.rejects(fetch(`http://127.0.0.1:${run.backendPort}`));
},
);
await test("does not start Vite when the backend fails readiness", { timeout: 10_000 }, async (context) => {
const run = await launcher(context, { S2S_TEST_FAIL: "backend" });
assert.equal(await run.exited, 1, run.output());
await assert.rejects(readFile(join(run.root, "frontend.json")));
assert.match(run.output(), /FastAPI exited before it was ready/);
});
await test("does not start either server when uv sync fails", { timeout: 10_000 }, async (context) => {
const run = await launcher(context, { S2S_TEST_UV_EXIT: "23" });
assert.equal(await run.exited, 23, run.output());
await assert.rejects(readFile(join(run.root, "backend.json")));
await assert.rejects(readFile(join(run.root, "frontend.json")));
});
await test("stops Vite if the backend exits after startup", { timeout: 10_000 }, async (context) => {
const run = await launcher(context);
await waitForFile(join(run.root, "frontend-worker-ready"));
const backendPid = Number(await readFile(join(run.root, "backend.pid"), "utf8"));
assert.ok(Number.isInteger(backendPid) && backendPid > 0);
process.kill(-backendPid, "SIGTERM");
assert.equal(await run.exited, 1, run.output());
assert.equal(await waitForFile(join(run.root, "frontend-worker-stopped")), "yes");
await assert.rejects(fetch(`http://127.0.0.1:${run.publicPort}`));
});
await test("rejects an invalid public port before starting either server", { timeout: 10_000 }, async (context) => {
const run = await launcher(context, { UI_PORT: "65536" });
assert.equal(await run.exited, 1);
assert.match(run.output(), /UI_PORT must be a port/);
await assert.rejects(readFile(join(run.root, "backend.json")));
});