skalpel 4.0.41 → 4.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ // verify-core.test.mjs — pure-function unit tests for the three CRITICAL-LOGIC functions the mutation
2
+ // harness (mutation-check.mjs) targets: detectCompletionClaim, classifyOutcome, classifyConfirm. These
3
+ // are pure (no fs / no network), so they need no HOME isolation. Every assertion here must be tight enough
4
+ // that flipping a predicate/branch in the function under test makes at least one case fail (survivors = 0).
5
+ import { test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import {
8
+ detectCompletionClaim,
9
+ classifyOutcome,
10
+ classifyConfirm,
11
+ hasMutatingFlag,
12
+ urlResponded,
13
+ shipUrlPass,
14
+ } from "./verify-shadow.mjs";
15
+
16
+ test("detectCompletionClaim — fires on real completion claims", () => {
17
+ for (const s of [
18
+ "All tests pass now.",
19
+ "I've fixed the bug.",
20
+ "Done — build is green.",
21
+ "lgtm",
22
+ "everything should work now",
23
+ "ready to merge",
24
+ ]) {
25
+ assert.equal(detectCompletionClaim(s).claim, true, `should claim: ${s}`);
26
+ }
27
+ });
28
+
29
+ test("detectCompletionClaim — does NOT fire on non-claims", () => {
30
+ for (const s of [
31
+ "Let me run the tests and see.",
32
+ "This might be broken.",
33
+ "Can you check whether it works?",
34
+ "",
35
+ " ",
36
+ ]) {
37
+ assert.equal(detectCompletionClaim(s).claim, false, `should NOT claim: ${s}`);
38
+ }
39
+ });
40
+
41
+ test("detectCompletionClaim — returns the specific claim line, bounded to 120 chars", () => {
42
+ const r = detectCompletionClaim("Here is a summary.\nAll tests pass.\nMore prose.");
43
+ assert.equal(r.claim, true);
44
+ assert.equal(r.text, "All tests pass.");
45
+ const long = "x ".repeat(200) + "all tests pass";
46
+ assert.ok(detectCompletionClaim(long).text.length <= 120);
47
+ });
48
+
49
+ test("classifyOutcome — no error is a PASS", () => {
50
+ assert.equal(classifyOutcome(null, ""), "PASS");
51
+ assert.equal(classifyOutcome(undefined, "anything"), "PASS");
52
+ });
53
+
54
+ test("classifyOutcome — a real non-zero exit is a GENUINE_FAIL", () => {
55
+ assert.equal(classifyOutcome({ code: 1 }, "2 failed, 3 passed"), "GENUINE_FAIL");
56
+ assert.equal(
57
+ classifyOutcome({ code: 2 }, "AssertionError: expected 1 to equal 2"),
58
+ "GENUINE_FAIL",
59
+ );
60
+ });
61
+
62
+ test("classifyOutcome — broken harness is NEVER a lie (HARNESS_ERROR)", () => {
63
+ assert.equal(classifyOutcome({ killed: true, signal: "SIGTERM" }, "..."), "HARNESS_ERROR"); // timeout
64
+ assert.equal(classifyOutcome({ code: "ENOENT" }, "..."), "HARNESS_ERROR"); // missing binary/cwd
65
+ assert.equal(classifyOutcome({ code: 127 }, "..."), "HARNESS_ERROR"); // command not found
66
+ assert.equal(classifyOutcome({ code: 126 }, "..."), "HARNESS_ERROR"); // not executable
67
+ assert.equal(classifyOutcome({ code: 1 }, 'npm error Missing script: "test"'), "HARNESS_ERROR");
68
+ assert.equal(classifyOutcome({ code: 1 }, "bash: pytest: command not found"), "HARNESS_ERROR");
69
+ });
70
+
71
+ test("classifyConfirm — 2/2 GENUINE_FAIL is the ONLY confirmed catch", () => {
72
+ const two = classifyConfirm("GENUINE_FAIL", "GENUINE_FAIL");
73
+ assert.deepEqual(two, { needsConfirm: true, confirmedFail: true, unconfirmedFail: false });
74
+ });
75
+
76
+ test("classifyConfirm — a single un-reproduced fail is UNCONFIRMED (never a red)", () => {
77
+ for (const second of ["PASS", "HARNESS_ERROR", "REFUSED", null, undefined]) {
78
+ const r = classifyConfirm("GENUINE_FAIL", second);
79
+ assert.equal(r.needsConfirm, true, `needsConfirm for second=${second}`);
80
+ assert.equal(r.confirmedFail, false, `NOT confirmed for second=${second}`);
81
+ assert.equal(r.unconfirmedFail, true, `unconfirmed for second=${second}`);
82
+ }
83
+ });
84
+
85
+ test("classifyConfirm — a non-fail first run needs no confirm and is never a catch", () => {
86
+ for (const first of ["PASS", "HARNESS_ERROR", "REFUSED"]) {
87
+ const r = classifyConfirm(first, "GENUINE_FAIL");
88
+ assert.deepEqual(
89
+ r,
90
+ { needsConfirm: false, confirmedFail: false, unconfirmedFail: false },
91
+ `first=${first}`,
92
+ );
93
+ }
94
+ });
95
+
96
+ // ---- Fix 2: negation / hedge guard — a negated claim is NOT a completion claim ----
97
+ test("detectCompletionClaim — NEGATED / hedged lines are NOT completion claims (no self-contradicting red)", () => {
98
+ for (const s of [
99
+ "not done yet, still working on it",
100
+ "it isn't done",
101
+ "this isn't fixed",
102
+ "won't be fixed in this PR",
103
+ "not fixed yet",
104
+ "the tests are still failing",
105
+ "still broken, needs another pass",
106
+ "tests are still red",
107
+ "TODO: implement the parser",
108
+ "to-do: wire the handler",
109
+ "almost done",
110
+ "not quite passing yet",
111
+ "the build does not pass",
112
+ ]) {
113
+ assert.equal(detectCompletionClaim(s).claim, false, `should NOT claim (negated): ${s}`);
114
+ }
115
+ });
116
+
117
+ test("detectCompletionClaim — a genuine claim still fires even alongside a negated line", () => {
118
+ assert.equal(detectCompletionClaim("I've fixed the bug and it works now.").claim, true);
119
+ const r = detectCompletionClaim("The refactor is done.\nThe auth part is not done yet.");
120
+ assert.equal(r.claim, true);
121
+ assert.equal(r.text, "The refactor is done.", "picks the non-negated positive line");
122
+ });
123
+
124
+ // ---- Fix 3: mutating-flag guard — a proof that rewrites files is refused ----
125
+ test("hasMutatingFlag — flags that REWRITE files are caught", () => {
126
+ for (const [cmd, args] of [
127
+ ["eslint", ["--fix"]],
128
+ ["eslint", ["src", "--fix"]],
129
+ ["ruff", ["check", "--fix"]],
130
+ ["ruff", ["format"]],
131
+ ["ruff", ["format", "app.py"]],
132
+ ["jest", ["-u"]],
133
+ ["jest", ["--updateSnapshot"]],
134
+ ["vitest", ["--update"]],
135
+ ["vitest", ["-u"]],
136
+ ["biome", ["check", "--write"]],
137
+ ["prettier", ["--write", "."]],
138
+ ]) {
139
+ assert.equal(hasMutatingFlag(cmd, args), true, `mutating: ${cmd} ${args.join(" ")}`);
140
+ }
141
+ });
142
+
143
+ test("hasMutatingFlag — pure verify runs (and pnpm -w) are NOT mutating", () => {
144
+ for (const [cmd, args] of [
145
+ ["eslint", ["."]],
146
+ ["ruff", ["check"]],
147
+ ["ruff", ["format", "--check"]],
148
+ ["ruff", ["format", "--diff"]],
149
+ ["jest", []],
150
+ ["vitest", ["run"]],
151
+ ["biome", ["check"]],
152
+ ["pnpm", ["-w", "test"]], // -w = workspace-root, NOT write
153
+ ["npm", ["test"]],
154
+ ]) {
155
+ assert.equal(hasMutatingFlag(cmd, args), false, `NOT mutating: ${cmd} ${args.join(" ")}`);
156
+ }
157
+ });
158
+
159
+ // ---- Fix 1: SHIP-STATUS URL reachability — any HTTP status = live (never a red) ----
160
+ test("shipUrlPass — any HTTP status means REACHABLE/live → NO red", () => {
161
+ for (const code of [200, 301, 302, 401, 403, 404, 429, 500, 502, 503]) {
162
+ assert.equal(shipUrlPass(`http_code=${code}`, null), true, `HTTP ${code} → reachable (no red)`);
163
+ }
164
+ });
165
+
166
+ test("shipUrlPass — a red requires 2/2 NO-RESPONSE; a single transient blip does not", () => {
167
+ assert.equal(
168
+ shipUrlPass("curl error: timeout", "curl error: Could not resolve host"),
169
+ false,
170
+ "2/2 no-response → decisive unreachable (red)",
171
+ );
172
+ assert.equal(
173
+ shipUrlPass("curl error: timeout", "http_code=200"),
174
+ true,
175
+ "no-response then response → live (no red)",
176
+ );
177
+ assert.equal(shipUrlPass("no http code: 000", "no http code: 000"), false, "000 twice → red");
178
+ });
179
+
180
+ test("urlResponded — http_code=<n> means the server answered; curl errors do not", () => {
181
+ assert.equal(urlResponded("http_code=503"), true);
182
+ assert.equal(urlResponded("http_code=200"), true);
183
+ assert.equal(urlResponded("curl error: timeout"), false);
184
+ assert.equal(urlResponded("no http code: 000"), false);
185
+ assert.equal(urlResponded(""), false);
186
+ });
@@ -0,0 +1,194 @@
1
+ // verify-integration.test.mjs — END-TO-END proofs that drive the REAL engine: recordVerifyShadow
2
+ // reconstructs the session's own `npm test`, re-runs it for real (execFile, allowlist, 60s cap), and the
3
+ // false-accusation safeguards gate the REAL reveal state file. Also exercises the real `skalpel yes|no`
4
+ // CLI. HOME-isolated (import "./_test-home.mjs" FIRST). Requires `npm` on PATH (present in node:20/22).
5
+ import "./_test-home.mjs";
6
+ import { test, beforeEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "node:fs";
9
+ import { tmpdir, homedir } from "node:os";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { spawnSync } from "node:child_process";
13
+ import { recordVerifyShadow } from "./verify-shadow.mjs";
14
+ import { appendAdjudication } from "./insights.mjs";
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const DIR = join(homedir(), ".skalpel");
18
+ const REVEAL = join(DIR, "verify-reveal.json");
19
+ const LOG = join(DIR, "verify-shadow.log");
20
+ const INSIGHTS = join(DIR, "insights.ndjson");
21
+ const ADJ = join(DIR, "verify-adjudications.ndjson");
22
+ const SUPPRESS = join(DIR, "verify-suppressed.ndjson");
23
+
24
+ beforeEach(() => {
25
+ rmSync(DIR, { recursive: true, force: true });
26
+ mkdirSync(DIR, { recursive: true });
27
+ process.env.SKALPEL_VERIFY_REVEAL = "1"; // ARMED for these end-to-end tests
28
+ });
29
+
30
+ // A temp project whose `npm test` outcome we control via a `mode` file + a per-run counter.
31
+ function makeProject(mode) {
32
+ const p = mkdtempSync(join(tmpdir(), "skalpel-proj-"));
33
+ writeFileSync(
34
+ join(p, "package.json"),
35
+ JSON.stringify({ name: "t", version: "1.0.0", scripts: { test: "node run-test.js" } }),
36
+ );
37
+ writeFileSync(
38
+ join(p, "run-test.js"),
39
+ `const fs=require("fs");const mode=fs.readFileSync("mode","utf8").trim();
40
+ let n=0;try{n=parseInt(fs.readFileSync("n","utf8"),10)||0;}catch{}
41
+ n++;fs.writeFileSync("n",String(n));
42
+ if(mode==="always_fail"){console.log("assertion failed on run "+n);process.exit(1);}
43
+ if(mode==="flake"){if(n===1){console.log("assertion failed on run "+n);process.exit(1);}console.log("ok "+n);process.exit(0);}
44
+ console.log("ok "+n);process.exit(0);`,
45
+ );
46
+ writeFileSync(join(p, "mode"), mode);
47
+ return p;
48
+ }
49
+
50
+ // A minimal Claude transcript: a Bash tool_use that ran `npm test`, then an assistant completion claim.
51
+ function writeTranscript(proj, claimText) {
52
+ const t = join(proj, "transcript.jsonl");
53
+ const rows = [
54
+ {
55
+ type: "assistant",
56
+ cwd: proj,
57
+ message: {
58
+ role: "assistant",
59
+ content: [{ type: "tool_use", name: "Bash", id: "tool_1", input: { command: "npm test" } }],
60
+ },
61
+ },
62
+ {
63
+ type: "user",
64
+ message: {
65
+ role: "user",
66
+ content: [{ type: "tool_result", tool_use_id: "tool_1", content: "ran" }],
67
+ },
68
+ },
69
+ {
70
+ type: "assistant",
71
+ message: { role: "assistant", content: [{ type: "text", text: claimText }] },
72
+ },
73
+ ];
74
+ writeFileSync(t, rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
75
+ return t;
76
+ }
77
+
78
+ const lastRow = () => {
79
+ const ls = readFileSync(LOG, "utf8").trim().split("\n");
80
+ return JSON.parse(ls[ls.length - 1]);
81
+ };
82
+ const insightKinds = () => {
83
+ try {
84
+ return readFileSync(INSIGHTS, "utf8")
85
+ .trim()
86
+ .split("\n")
87
+ .filter(Boolean)
88
+ .map((l) => JSON.parse(l));
89
+ } catch {
90
+ return [];
91
+ }
92
+ };
93
+
94
+ async function drive(mode, { session, claim }) {
95
+ rmSync(REVEAL, { force: true });
96
+ const proj = makeProject(mode);
97
+ const tp = writeTranscript(proj, claim);
98
+ await recordVerifyShadow({ transcriptPath: tp, session, cwd: proj });
99
+ return { revealed: existsSync(REVEAL), row: lastRow() };
100
+ }
101
+
102
+ test("2/2 flake-confirm — a confirmed fail SURFACES a red", async () => {
103
+ const r = await drive("always_fail", { session: "s-fail", claim: "All tests pass. Done." });
104
+ assert.equal(r.revealed, true, "confirmed 2/2 fail → red reveal written");
105
+ assert.equal(r.row.mismatch, true);
106
+ assert.equal(r.row.outcome, "GENUINE_FAIL");
107
+ assert.equal(r.row.confirm_outcome, "GENUINE_FAIL");
108
+ assert.equal(r.row.unconfirmed_fail, false);
109
+ });
110
+
111
+ test("2/2 flake-confirm — a single un-reproduced fail NEVER surfaces (flake, logged UNCONFIRMED)", async () => {
112
+ const r = await drive("flake", { session: "s-flake", claim: "All tests pass now." });
113
+ assert.equal(r.revealed, false, "1-fail-then-pass → NO red");
114
+ assert.equal(r.row.mismatch, false);
115
+ assert.equal(r.row.outcome, "GENUINE_FAIL");
116
+ assert.equal(r.row.confirm_outcome, "PASS");
117
+ assert.equal(r.row.unconfirmed_fail, true, "recorded for the instrument, never shown");
118
+ });
119
+
120
+ test("2/2 flake-confirm — a clean PASS surfaces no red (green verdict path)", async () => {
121
+ const r = await drive("always_pass", { session: "s-pass", claim: "Done — all green." });
122
+ assert.equal(r.revealed, false);
123
+ assert.equal(r.row.outcome, "PASS");
124
+ assert.equal(r.row.confirm_outcome, null, "no confirm run needed on a pass");
125
+ });
126
+
127
+ test("suppression — `skalpel no` stops that exact proof re-accusing for the session", async () => {
128
+ // Run 1: a confirmed fail surfaces the red and logs a shown-reveal row for the CLI to adjudicate.
129
+ const first = await drive("always_fail", { session: "s1", claim: "All tests pass." });
130
+ assert.equal(first.revealed, true);
131
+
132
+ // The user hits `skalpel no` (the REAL CLI) — appends the adjudication + suppresses (s1, npm test).
133
+ const no = spawnSync("node", [join(HERE, "skalpel-setup.mjs"), "no"], {
134
+ env: process.env,
135
+ encoding: "utf8",
136
+ });
137
+ assert.equal(no.status, 0, no.stderr);
138
+ const adj = readFileSync(ADJ, "utf8").trim();
139
+ assert.match(adj, /"verdict":"no"/);
140
+ assert.ok(existsSync(SUPPRESS), "suppress file written");
141
+ assert.match(readFileSync(SUPPRESS, "utf8"), /npm test/);
142
+
143
+ // Run 2: SAME session, a DIFFERENT claim (so it isn't deduped) — still a confirmed 2/2 fail, but the
144
+ // proof is suppressed → NO new red, and a reveal_suppressed insight explains why.
145
+ const second = await drive("always_fail", { session: "s1", claim: "The build is green now." });
146
+ assert.equal(second.row.mismatch, true, "still a real confirmed catch in the instrument");
147
+ assert.equal(second.revealed, false, "…but suppressed → no red shown");
148
+ const supp = insightKinds().filter(
149
+ (i) => i.kind === "reveal_suppressed" && i.reason === "adjudicated_no",
150
+ );
151
+ assert.ok(supp.length >= 1, "reveal_suppressed(adjudicated_no) recorded");
152
+ });
153
+
154
+ test("auto-darken — <90% over >=10 adjudications pauses the LIVE red (falls back to retro)", async () => {
155
+ // 8 right / 2 wrong = 80% over 10 rows → derived auto-darken = true.
156
+ for (let i = 0; i < 8; i++) appendAdjudication({ verdict: "yes" });
157
+ for (let i = 0; i < 2; i++) appendAdjudication({ verdict: "no" });
158
+
159
+ const r = await drive("always_fail", { session: "s-dark", claim: "All tests pass." });
160
+ assert.equal(r.row.mismatch, true, "still a real confirmed catch in the instrument");
161
+ assert.equal(r.revealed, false, "auto-darkened → no live red");
162
+ const supp = insightKinds().filter(
163
+ (i) => i.kind === "reveal_suppressed" && i.reason === "auto_darken",
164
+ );
165
+ assert.ok(supp.length >= 1, "reveal_suppressed(auto_darken) recorded");
166
+ });
167
+
168
+ test("`skalpel yes` — records a real catch toward precision (no suppression)", async () => {
169
+ await drive("always_fail", { session: "s-yes", claim: "All tests pass." });
170
+ const yes = spawnSync("node", [join(HERE, "skalpel-setup.mjs"), "yes"], {
171
+ env: process.env,
172
+ encoding: "utf8",
173
+ });
174
+ assert.equal(yes.status, 0, yes.stderr);
175
+ assert.match(readFileSync(ADJ, "utf8"), /"verdict":"yes"/);
176
+ assert.equal(existsSync(SUPPRESS), false, "a `yes` never suppresses");
177
+ });
178
+
179
+ test("`skalpel no` with nothing to adjudicate is honest, not an error", () => {
180
+ const no = spawnSync("node", [join(HERE, "skalpel-setup.mjs"), "no"], {
181
+ env: process.env,
182
+ encoding: "utf8",
183
+ });
184
+ assert.equal(no.status, 0);
185
+ assert.match(no.stdout, /No recent skalpel catch/i);
186
+ assert.equal(existsSync(ADJ), false);
187
+ });
188
+
189
+ test("fail-open — the detached worker never throws on a broken transcript, surfaces nothing", async () => {
190
+ await assert.doesNotReject(
191
+ recordVerifyShadow({ transcriptPath: "/no/such/transcript.jsonl", session: "x", cwd: "/tmp" }),
192
+ );
193
+ assert.equal(existsSync(REVEAL), false, "no red on a broken input");
194
+ });
@@ -0,0 +1,178 @@
1
+ // verify-safeguards.test.mjs — the false-accusation safeguards at the derivation level: the arming
2
+ // resolver precedence, the adjudication ledger + <90% auto-darken derivation, and the session-scoped
3
+ // suppression. HOME-isolated (import "./_test-home.mjs" FIRST) so nothing here can read/clobber a real
4
+ // ~/.skalpel. Every function under test is fail-open: a corrupt file must degrade to safe, never throw.
5
+ import "./_test-home.mjs";
6
+ import { test, beforeEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { rmSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { resolveConsentChoice } from "./skalpel-setup.mjs";
12
+ import {
13
+ revealEnabled,
14
+ verifySpawnArmed,
15
+ verifyConfigArmed,
16
+ adjudicationStats,
17
+ autoDarkened,
18
+ appendAdjudication,
19
+ suppressProof,
20
+ isProofSuppressed,
21
+ proofSig,
22
+ appendRevealLog,
23
+ lastReveal,
24
+ verifyArmedAt,
25
+ AUTO_DARKEN_MIN_ADJ,
26
+ ADJUDICATIONS_PATH,
27
+ CLIENT_JSON_PATH,
28
+ SUPPRESS_PATH,
29
+ } from "./insights.mjs";
30
+
31
+ const DIR = join(homedir(), ".skalpel");
32
+ beforeEach(() => {
33
+ rmSync(DIR, { recursive: true, force: true });
34
+ mkdirSync(DIR, { recursive: true });
35
+ delete process.env.SKALPEL_VERIFY_REVEAL;
36
+ delete process.env.SKALPEL_VERIFY_SHADOW;
37
+ });
38
+ const writeCfg = (o) => writeFileSync(CLIENT_JSON_PATH, JSON.stringify(o));
39
+
40
+ // ---- arming resolver precedence: env > config > default off -------------------------------------
41
+ test("arming — default OFF when config absent and env unset", () => {
42
+ assert.equal(verifyConfigArmed(), null);
43
+ assert.equal(revealEnabled({}), false);
44
+ assert.equal(verifySpawnArmed({}), false);
45
+ });
46
+
47
+ test("arming — consented config on/off", () => {
48
+ writeCfg({ verify: "on" });
49
+ assert.equal(revealEnabled({}), true);
50
+ assert.equal(verifySpawnArmed({}), true);
51
+ writeCfg({ verify: "off" });
52
+ assert.equal(revealEnabled({}), false);
53
+ });
54
+
55
+ test("arming — env override BEATS config in both directions", () => {
56
+ writeCfg({ verify: "on" });
57
+ assert.equal(revealEnabled({ SKALPEL_VERIFY_REVEAL: "0" }), false, "env off beats config on");
58
+ writeCfg({ verify: "off" });
59
+ assert.equal(revealEnabled({ SKALPEL_VERIFY_REVEAL: "1" }), true, "env on beats config off");
60
+ });
61
+
62
+ test("arming — SKALPEL_VERIFY_SHADOW arms the spawn but NOT the reveal", () => {
63
+ writeCfg({});
64
+ assert.equal(verifySpawnArmed({ SKALPEL_VERIFY_SHADOW: "1" }), true);
65
+ assert.equal(revealEnabled({ SKALPEL_VERIFY_SHADOW: "1" }), false);
66
+ });
67
+
68
+ test("arming — fail-open: a corrupt client.json never throws, resolves OFF", () => {
69
+ writeFileSync(CLIENT_JSON_PATH, "{ this is not json");
70
+ assert.doesNotThrow(() => revealEnabled({}));
71
+ assert.equal(verifyConfigArmed(), null);
72
+ assert.equal(revealEnabled({}), false);
73
+ });
74
+
75
+ // ---- adjudication ledger + <90% auto-darken derivation ------------------------------------------
76
+ test("adjudicationStats — precision = right/(right+wrong), null when empty", () => {
77
+ assert.deepEqual(adjudicationStats(), { right: 0, wrong: 0, total: 0, precision: null });
78
+ for (let i = 0; i < 9; i++) appendAdjudication({ verdict: "yes" });
79
+ appendAdjudication({ verdict: "no" });
80
+ const s = adjudicationStats();
81
+ assert.equal(s.right, 9);
82
+ assert.equal(s.wrong, 1);
83
+ assert.equal(s.total, 10);
84
+ assert.ok(Math.abs(s.precision - 0.9) < 1e-9);
85
+ });
86
+
87
+ test("auto-darken — needs BOTH >= min adjudications AND precision < 90%", () => {
88
+ // 8 wrong of 9 → precision high but too few rows → NOT darkened.
89
+ appendAdjudication({ verdict: "yes" });
90
+ for (let i = 0; i < 8; i++) appendAdjudication({ verdict: "no" });
91
+ assert.equal(adjudicationStats().total, 9);
92
+ assert.equal(autoDarkened(), false, "9 rows < min → not darkened");
93
+
94
+ // Exactly 90% over >= min stays LIVE (only strictly below darkens).
95
+ rmSync(ADJUDICATIONS_PATH, { force: true });
96
+ for (let i = 0; i < 9; i++) appendAdjudication({ verdict: "yes" });
97
+ appendAdjudication({ verdict: "no" }); // 9/10 = 90%
98
+ assert.equal(autoDarkened(), false, "exactly 90% stays live");
99
+
100
+ // 8/10 = 80% over >= min → DARKENED.
101
+ appendAdjudication({ verdict: "no" }); // now 9 yes / 2 no = 81.8%... push below with more no
102
+ for (let i = 0; i < 2; i++) appendAdjudication({ verdict: "no" }); // 9 yes / 4 no = 69%
103
+ assert.ok(adjudicationStats().total >= AUTO_DARKEN_MIN_ADJ);
104
+ assert.ok(adjudicationStats().precision < 0.9);
105
+ assert.equal(autoDarkened(), true, "below 90% over min → darkened");
106
+ });
107
+
108
+ test("auto-darken — fail-open: garbage rows never throw, unparseable → safe (not darkened)", () => {
109
+ appendFileSync(ADJUDICATIONS_PATH, "not json\n{bad\n");
110
+ assert.doesNotThrow(() => autoDarkened());
111
+ assert.equal(autoDarkened(), false);
112
+ });
113
+
114
+ // ---- session-scoped suppression ------------------------------------------------------------------
115
+ test("suppression — a suppressed (session, proof) matches; a different session/proof does NOT", () => {
116
+ assert.equal(isProofSuppressed("s1", "npm test"), false);
117
+ suppressProof("s1", "npm test");
118
+ assert.equal(isProofSuppressed("s1", "npm test"), true, "same session+proof suppressed");
119
+ assert.equal(isProofSuppressed("s1", " npm test "), true, "whitespace-normalized match");
120
+ assert.equal(isProofSuppressed("s2", "npm test"), false, "different session NOT suppressed");
121
+ assert.equal(isProofSuppressed("s1", "pnpm build"), false, "different proof NOT suppressed");
122
+ });
123
+
124
+ test("suppression — a null/empty session is never suppressed (no over-suppression)", () => {
125
+ assert.equal(suppressProof(null, "npm test"), false);
126
+ assert.equal(suppressProof("s1", ""), false);
127
+ assert.equal(isProofSuppressed(null, "npm test"), false);
128
+ assert.equal(isProofSuppressed("", "npm test"), false);
129
+ });
130
+
131
+ test("suppression — fail-open: a corrupt suppress file never throws", () => {
132
+ writeFileSync(SUPPRESS_PATH, "garbage\n");
133
+ assert.doesNotThrow(() => isProofSuppressed("s1", "npm test"));
134
+ assert.equal(isProofSuppressed("s1", "npm test"), false);
135
+ });
136
+
137
+ test("proofSig — normalizes whitespace and bounds length", () => {
138
+ assert.equal(proofSig(" npm test \n"), "npm test");
139
+ assert.ok(proofSig("x ".repeat(500)).length <= 200);
140
+ });
141
+
142
+ // ---- reveal log / lastReveal (the row `skalpel yes|no` adjudicates) ------------------------------
143
+ test("lastReveal — returns the most-recent shown reveal, or null when none", () => {
144
+ assert.equal(lastReveal(), null);
145
+ appendRevealLog({ session: "s1", proof_command: "npm test", line: "first" });
146
+ appendRevealLog({ session: "s2", proof_command: "pnpm build", line: "second" });
147
+ const r = lastReveal();
148
+ assert.equal(r.session, "s2");
149
+ assert.equal(r.proof_command, "pnpm build");
150
+ });
151
+
152
+ // ---- armed-days source (a recorded real timestamp, not a metric) --------------------------------
153
+ test("verifyArmedAt — reads the recorded ISO stamp, null when absent/garbage", () => {
154
+ assert.equal(verifyArmedAt(), null);
155
+ const iso = "2026-01-01T00:00:00.000Z";
156
+ writeCfg({ verify: "on", verify_armed_at: iso });
157
+ assert.equal(verifyArmedAt().toISOString(), iso);
158
+ writeCfg({ verify: "on", verify_armed_at: "not-a-date" });
159
+ assert.equal(verifyArmedAt(), null);
160
+ });
161
+
162
+ // ---- Fix 4: `verify off` durable against a setup re-run (default = existing setting) ----
163
+ test("consent — an existing OFF is NEVER silently re-armed by a setup Enter", () => {
164
+ // The deliberate opt-out: Enter (empty) must KEEP it off.
165
+ assert.equal(resolveConsentChoice("off", ""), "off", "off + Enter stays off");
166
+ assert.equal(resolveConsentChoice("off", "n"), "off", "off + n stays off");
167
+ // …but an explicit yes re-arms.
168
+ assert.equal(resolveConsentChoice("off", "y"), "on", "off + y turns on");
169
+ assert.equal(resolveConsentChoice("off", "on"), "on");
170
+ });
171
+
172
+ test("consent — on/unset default to ON (the activation intent), n declines", () => {
173
+ assert.equal(resolveConsentChoice(undefined, ""), "on", "unset + Enter → on");
174
+ assert.equal(resolveConsentChoice(null, ""), "on", "null + Enter → on");
175
+ assert.equal(resolveConsentChoice("on", ""), "on", "on + Enter stays on");
176
+ assert.equal(resolveConsentChoice("on", "n"), "off", "on + n turns off");
177
+ assert.equal(resolveConsentChoice("on", "no"), "off");
178
+ });