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((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 = {}) { const root = await mkdtemp(join(tmpdir(), "s2s-launcher-")); const frontend = join(root, "frontend-s2s"); const modules = join(frontend, "node_modules"); await mkdir(join(root, ".venv/bin"), { recursive: true }); await mkdir(join(root, "s2s-client")); 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, "s2s-client/requirements.txt"), ""); await writeFile(join(root, ".venv/bin/activate"), 'export PATH="$S2S_TEST_ROOT/.venv/bin:$PATH"\n'); await writeFile( join(root, ".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, 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((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); 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("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"))); });