import { spawn } from "node:child_process"; import { createWriteStream, mkdirSync, mkdtempSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; export function parseOptions(env = process.env) { const workers = positive(env.E2E_WORKERS ?? "1", "E2E_WORKERS"); const seed = Number(env.E2E_SEED ?? "1"); if ( !/^\d+$/.test(env.E2E_SEED ?? "1") || !Number.isInteger(seed) || seed < 0 || seed + workers - 1 > 0xffffffff ) { throw new Error("E2E_SEED range must fit uint32"); } return { workers, seed, iterations: positive(env.E2E_ITERATIONS ?? "25", "E2E_ITERATIONS"), timeout: positive(env.E2E_TIMEOUT_SECONDS ?? "900", "E2E_TIMEOUT_SECONDS") * 1000, }; } export async function runCommand( command, args, { cwd, logPath, timeout, active = new Set(), env = process.env }, ) { const log = createWriteStream(logPath, { flags: "w" }); const child = spawn(command, args, { cwd, env: { ...env, NO_COLOR: "1", FORCE_COLOR: "0" }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", }); let logError; log.on("error", (error) => { logError = error; killOwned(child, "SIGTERM"); }); active.add(child); child.stdout.pipe(log, { end: false }); child.stderr.pipe(log, { end: false }); let timedOut = false; let hardKill; const timer = setTimeout(() => { timedOut = true; killOwned(child, "SIGTERM"); hardKill = setTimeout(() => killOwned(child, "SIGKILL"), 5_000); }, timeout); let spawnError; child.once("error", (error) => { spawnError = error; }); try { const result = await new Promise((resolveResult) => child.once("close", (code, signal) => resolveResult({ code, signal }), ), ); if (!logError) { await new Promise((resolveLog) => { log.once("error", resolveLog); log.end(resolveLog); }); } return { command, args, ...result, timedOut, error: spawnError?.message ?? logError?.message, success: result.code === 0 && !timedOut && !spawnError && !logError, }; } finally { clearTimeout(timer); clearTimeout(hardKill); active.delete(child); } } function killOwned(child, signal) { if (!child.pid) { return; } try { if (process.platform === "win32") { child.kill(signal); } else { process.kill(-child.pid, signal); // the group created by this spawn } } catch (error) { if (error.code !== "ESRCH") { throw error; } } } function positive(value, label) { const result = Number(value); if ( !/^\d+$/.test(String(value)) || !Number.isSafeInteger(result) || result < 1 ) throw new Error(`${label} must be a positive integer`); return result; }; async function main() { const options = parseOptions(); const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const artifacts = process.env.E2E_ARTIFACTS ? resolve(process.env.E2E_ARTIFACTS) : mkdtempSync(join(tmpdir(), "vault-link-e2e-")); mkdirSync(artifacts, { recursive: true }); console.log(`Artifacts: ${artifacts}`); console.log( `Seeds: ${options.seed}..${options.seed + options.workers - 1}; iterations: ${options.iterations}`, ); const active = new Set(); let interrupted = false; const stop = () => { interrupted = true; for (const child of active) killOwned(child, "SIGTERM"); }; process.on("SIGINT", stop); process.on("SIGTERM", stop); process.on("exit", () => { for (const child of active) killOwned(child, "SIGKILL"); }); const results = []; const run = async (name, command, args, cwd = root) => { if (interrupted) { throw new Error("Interrupted"); } const logPath = join(artifacts, `${name}.log`); const result = await runCommand(command, args, { cwd, logPath, timeout: options.timeout, active, env: { ...process.env, E2E_ARTIFACTS: artifacts }, }); results.push({ name, ...result }); writeFileSync( join(artifacts, "results.json"), JSON.stringify({ options, results }, null, 2), ); console.log(`${result.success ? "PASS" : "FAIL"} ${name}: ${logPath}`); return result.success; }; const frontend = join(root, "frontend"); if ( !(await run( "build-server", "cargo", ["build", "--release"], join(root, "sync-server"), )) ) { throw new Error("Server build failed"); } if ( !(await run( "build-clients", "npm", [ "run", "build", ], frontend, )) ) { throw new Error( "Client build failed (run npm ci in frontend if dependencies are missing)", ); } await run( "server-unit-tests", "cargo", ["test"], join(root, "sync-server"), ); await run( "client-unit-tests", "npm", ["run", "test", "--workspace", "sync-client"], frontend, ); await run("adapter-unit-tests", "npm", ["run", "test"], frontend); await run("harness-self-tests", "npm", ["run", "test:harness"], frontend); await run("crash-and-protocol", "npm", ["run", "test:protocol"], frontend); await run("deterministic", process.execPath, [ join(frontend, "deterministic-tests/dist/cli.js"), "--concurrency", String(options.workers), ]); // Execute all suites even if one reports a regression; failure is never // inferred from log text and successful workers cannot mask failed ones. await Promise.all( Array.from({ length: options.workers }, (_, i) => run(`fuzz-${options.seed + i}`, process.execPath, [ join(frontend, "test-client/dist/cli.js"), "--seed", String(options.seed + i), "--iterations", String(options.iterations), "--artifacts", artifacts, ]), ), ); if (interrupted || results.some((result) => !result.success)) { throw new Error( `Tests failed; inspect ${join(artifacts, "results.json")}`, ); } console.log("All harness suites passed"); } if ( process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) { main().catch((error) => { console.error(error.message); process.exitCode = 1; }); }