Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011h6NB8JQtfPc6eRCefEzBB
117 lines
4.7 KiB
JavaScript
117 lines
4.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
const source = readFileSync(new URL("./render-and-composite.sh", import.meta.url), "utf8");
|
|
const gateStart = source.indexOf(
|
|
'if [[ -f "$PROJECT/plan.json" && -d "$PROJECT/frames_fg" ]]; then',
|
|
);
|
|
const gateEnd = source.indexOf("\n# Custom mode", gateStart);
|
|
assert.ok(gateStart >= 0 && gateEnd > gateStart, "occlusion gate must be present");
|
|
const gate = source.slice(gateStart, gateEnd);
|
|
|
|
function runGate(t, { output = "", exitCode, skip = false }) {
|
|
const root = mkdtempSync(join(tmpdir(), "hf-occlusion-gate-"));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const project = join(root, "project with spaces");
|
|
mkdirSync(join(project, "frames_fg"), { recursive: true });
|
|
writeFileSync(join(project, "plan.json"), "{}");
|
|
writeFileSync(
|
|
join(root, "check-occlusion.cjs"),
|
|
`const fs = require("node:fs");
|
|
fs.writeSync(1, ${JSON.stringify(output)});
|
|
fs.writeFileSync(process.argv[2] + "/analyzer-ready", "");
|
|
${exitCode === undefined ? "setInterval(() => {}, 1000);" : `process.exit(${exitCode});`}
|
|
`,
|
|
);
|
|
// Run the production gate with a ready analyzer before advancing its Bash clock.
|
|
// A silent hanging analyzer must abort, never leave a PASS in the gate ledger.
|
|
const harness = join(root, "gate.sh");
|
|
writeFileSync(
|
|
harness,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
PROJECT="$1"
|
|
GATES="$PROJECT/_gates.txt"; : > "$GATES"
|
|
OCC_PID=""
|
|
trap 'if [[ -n "$OCC_PID" ]]; then kill -9 "$OCC_PID" 2>/dev/null || true; wait "$OCC_PID" 2>/dev/null || true; fi' EXIT
|
|
sleep() {
|
|
${exitCode === undefined ? 'while [[ ! -f "$PROJECT/analyzer-ready" ]]; do command sleep 0.01; done\n SECONDS=$((SECONDS + 10))' : "command sleep 0.01"}
|
|
}
|
|
${gate}
|
|
echo continued
|
|
`,
|
|
);
|
|
const result = spawnSync("bash", [harness, project], {
|
|
encoding: "utf8",
|
|
timeout: 10000,
|
|
env: { ...process.env, OCCLUSION_SKIP: skip ? "1" : "0" },
|
|
});
|
|
assert.ifError(result.error);
|
|
return { ...result, ledger: readFileSync(join(project, "_gates.txt"), "utf8") };
|
|
}
|
|
|
|
const verdict = "[v2] fixture word-fail≥65% cap-fail≥55%\n";
|
|
const failure = "[v2] 1 cap(s) FAIL: caption\n";
|
|
|
|
test("a timeout before any verdict aborts without recording PASS", (t) => {
|
|
const result = runGate(t, {});
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.ledger, "");
|
|
assert.doesNotMatch(result.stdout, /continued/);
|
|
assert.match(result.stderr, /timed out before.*verdict/);
|
|
assert.doesNotMatch(result.stderr, /hung after verdict/);
|
|
});
|
|
|
|
test("partial analysis output cannot make a timeout pass", (t) => {
|
|
const result = runGate(t, { output: "[v2] measuring layout\n" });
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.ledger, "");
|
|
});
|
|
|
|
test("a pre-verdict timeout can only continue as an explicit override", (t) => {
|
|
const result = runGate(t, { skip: true });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.ledger, /occlusion\+overflow OVERRIDDEN/);
|
|
assert.doesNotMatch(result.ledger, /PASS/);
|
|
assert.match(result.stdout, /continued/);
|
|
});
|
|
|
|
test("a passing verdict still tolerates a native teardown hang", (t) => {
|
|
const result = runGate(t, { output: verdict });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.ledger, "occlusion+overflow PASS\n");
|
|
assert.match(result.stdout, /continued/);
|
|
});
|
|
|
|
test("a failing verdict still aborts after a native teardown hang", (t) => {
|
|
const result = runGate(t, { output: verdict + failure });
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.ledger, "");
|
|
assert.doesNotMatch(result.stdout, /continued/);
|
|
});
|
|
|
|
test("a normally completed check records PASS", (t) => {
|
|
const result = runGate(t, { output: verdict, exitCode: 0 });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.ledger, "occlusion+overflow PASS\n");
|
|
assert.doesNotMatch(result.stderr, /timed out|hung after verdict/);
|
|
});
|
|
|
|
test("a normally failed check aborts", (t) => {
|
|
const result = runGate(t, { output: verdict + failure, exitCode: 2 });
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.ledger, "");
|
|
assert.doesNotMatch(result.stderr, /timed out|hung after verdict/);
|
|
});
|
|
|
|
test("an analyzer error aborts without recording PASS", (t) => {
|
|
const result = runGate(t, { output: "[v2] cannot measure layout\n", exitCode: 1 });
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.ledger, "");
|
|
assert.doesNotMatch(result.stderr, /timed out|hung after verdict/);
|
|
});
|