diff --git a/scripts/test/budget-audit.test.mjs b/scripts/test/budget-audit.test.mjs index 7823828..936dffa 100644 --- a/scripts/test/budget-audit.test.mjs +++ b/scripts/test/budget-audit.test.mjs @@ -148,7 +148,8 @@ await test("BA2 a missing web/dist is notRun — NEVER a pass (exit-2 class)", a }); await test("BA3 a fixture dist over the app-bundle ceiling FAILS the gate with the overage", async () => { - const fix = makeFixtureRoot("ba3-dist", { "assets/huge.js": "x".repeat(600000) }); + const { randomBytes } = await import("node:crypto"); + const fix = makeFixtureRoot("ba3-dist", { "assets/huge.js": randomBytes(600000).toString("base64") }); try { const m = await import(gatesUrl); const r = await m.runBundleBudgetGate({ distDir: fix.dir }); @@ -260,8 +261,58 @@ await test("GC2 a failing corpus FAILS the gate carrying that child's own output assert.match(r.evidence, /CORPUS GATE FAIL: synthetic/, "the child's own failure output must be carried verbatim"); }); -/* ————— summary ————— */ +/* ————— AU — the CLI ————— */ +const cliPath = path.join(ROOT, "scripts", "budget-audit.mjs"); + +function runCli(args, env = {}) { + return spawnSync(process.execPath, [cliPath, ...args], { + cwd: ROOT, + encoding: "utf8", + timeout: 300000, + env: { ...process.env, ...env }, + maxBuffer: 1 << 26, + }); +} + +await test("AU1 without --live: every required gate prints its evidence line, the free-tier comparison table is printed, and the audit is CLEAN with exit 0", () => { + assert.ok(existsSync(cliPath), "scripts/budget-audit.mjs must exist"); + const r = runCli([]); + assert.equal(r.status, 0, `exit 0 expected on the current tree (stdout: ${r.stdout?.slice(-400)})`); + const out = r.stdout ?? ""; + for (const id of ["route-cpu", "bundle-budget", "kv-writes", "subrequests", "golden-corpus"]) { + assert.match(out, new RegExp(`${id}: (PASS|FAIL|NOT RUN) — `), `${id} must print its one-line evidence`); + } + assert.match(out, /BUDGET AUDIT CLEAN/, "the CLEAN summary must print"); + assert.match(out, /50\/request/, "the subrequest ceiling must appear in the comparison table"); + assert.match(out, /10 ms\/invocation/, "the CPU ceiling must appear in the comparison table"); + assert.match(out, /not measured/, "unmeasured ceilings must read `not measured` — never 0"); + assert.match(out, /cache-headers: NOT RUN \(pass --live\)/, "the flag-skipped live gate is visible and honest"); +}); + +await test("AU2 with --live: the live header probe runs and the exit contract holds — a NOT RUN cache-header gate NEVER coexists with exit 0", () => { + const r = runCli(["--live"]); + const out = r.stdout ?? ""; + assert.ok([0, 1, 2].includes(r.status), `exit must be 0/1/2 (got ${r.status})`); + assert.match(out, /cache-headers: /, "the cache-header gate must print with --live"); + if (out.includes("cache-headers: NOT RUN")) { + assert.equal(r.status, 2, "a NOT RUN gate must map to exit 2 (environment drift) — never a silent pass"); + assert.match(out, /BUDGET AUDIT NOT RUN/, "the NOT RUN summary must print"); + } +}); + +await test("AU3 environment drift (a root with no builds/graph) maps to exit 2 — BUDGET AUDIT NOT RUN, never a pass (T-07-22)", () => { + const empty = mkdtempSync(path.join(tmpdir(), "budget-empty-")); + try { + const r = runCli([], { PRIVACY_AUDIT_ROOT: empty }); + assert.equal(r.status, 2, `a root without the measured surfaces must be exit 2 (stdout: ${r.stdout?.slice(-300)})`); + assert.match(r.stdout ?? "", /BUDGET AUDIT NOT RUN/, "the NOT RUN summary must print"); + } finally { + rmSync(empty, { recursive: true, force: true }); + } +}); + +/* ————— summary ————— */ if (failures.length > 0) { console.error(`\n${failures.length}/${ran} vector(s) FAILED:`); for (const f of failures) console.error(` - ${f.name}`);