import { test } from "node:test"; import assert from "node:assert/strict"; import { emptyCommitted, fold, type Committed } from "../state.ts"; import { observation } from "../observations.ts"; import { emptyPolicy } from "./policy.ts"; import { featureDocRelPath, trackGate } from "./track.ts"; function routed(route: "inline" | "tracked" | "forge", slug = "demo"): Committed { return fold(emptyCommitted(), observation({ toolCallId: "d1", toolName: "nodd_declare", input: { intent: "change", route, slug }, isError: false, resultText: "", at: "t", })); } const write = { toolName: "write", input: { path: "src/a.ts" } }; const absent = () => false; const present = () => true; test("a missing feature doc blocks the first write, naming the exact path", () => { const decision = trackGate(routed("tracked"), write, emptyPolicy(), absent); assert.equal(decision.allow, false); assert.ok(decision.allow === false && decision.reason.includes(".nodd/demo/feature.md")); assert.ok(decision.allow === false && decision.remedy.action.includes("nodd_declare")); assert.ok(decision.allow === false && decision.remedy.escapeHatch === "/nodd-allow track"); }); test("a present feature doc allows the write", () => { assert.equal(trackGate(routed("tracked"), write, emptyPolicy(), present).allow, true); }); test("route forge is tracked too", () => { assert.equal(trackGate(routed("forge"), write, emptyPolicy(), absent).allow, false); }); // routing.go:94 — small, understood work creates no durable task artifacts. test("route inline never blocks", () => { assert.equal(trackGate(routed("inline"), write, emptyPolicy(), absent).allow, true); assert.equal(trackGate(routed("inline"), { toolName: "bash", input: { command: "echo x > f" } }, emptyPolicy(), absent).allow, true); }); test("mutating bash blocks while a test command does not", () => { const state = routed("tracked"); assert.equal(trackGate(state, { toolName: "bash", input: { command: "echo x > f" } }, emptyPolicy(), absent).allow, false); assert.equal(trackGate(state, { toolName: "bash", input: { command: "npm test" } }, emptyPolicy(), absent).allow, true); }); // A doc the model can rewrite is a doc the model can forge, and then evidence // is prose again. This holds even when the doc exists and the route is inline. test("file_path is read like path: pi's write tool sends either", () => { // `write.js:99` and `edit.js:92` both accept `file_path ?? path`, and pi's // own write tool sends `file_path`. Reading only `path` left every // path-scoped rule blind on the real tool: the .nodd/ guard below let a // direct write to the feature document through. const decision = trackGate( routed("tracked"), { toolName: "write", input: { file_path: "/repo/.nodd/demo/feature.md" } }, emptyPolicy(), present, ); assert.equal(decision.allow, false, "a write under .nodd/ is refused however pi names the argument"); }); test("a direct write to .nodd/** is always blocked, pointing at nodd_task", () => { for (const state of [routed("tracked"), routed("inline"), emptyCommitted()]) { for (const path of [".nodd/x/feature.md", ".nodd/demo/state.json", "/repo/.nodd/demo/feature.md"]) { const decision = trackGate(state, { toolName: "write", input: { path } }, emptyPolicy(), present); assert.equal(decision.allow, false, `${path} must be blocked`); assert.ok(decision.allow === false && decision.remedy.action.includes("nodd_task")); } } const edit = trackGate(routed("tracked"), { toolName: "edit", input: { path: ".nodd/demo/feature.md" } }, emptyPolicy(), present); assert.equal(edit.allow, false); }); // design.md says `.nodd/**` writes are "always blocked", but REQ: gate-framework // says flag semantics obey the kill switch "without exception", and // REQ: gate-kill-switch-semantics 3 says obey immediately without working // around it. The explicit "without exception" wins: a gate the user turned off // is off, including its doc-integrity half. Turning `track` off is the // documented way to hand-edit a feature doc. test("the flag off disables the whole gate, doc protection included", () => { const policy = { ...emptyPolicy(), config: { track: { enabled: false } } }; assert.equal(trackGate(routed("tracked"), write, policy, absent).allow, true); assert.equal( trackGate(routed("tracked"), { toolName: "write", input: { path: ".nodd/demo/feature.md" } }, policy, present).allow, true, "a disabled gate does not keep enforcing half of itself", ); }); test("an undeclared session does not block here — that is gate-classify's job", () => { assert.equal(trackGate(emptyCommitted(), write, emptyPolicy(), absent).allow, true); }); // --------------------------------------------------------------------------- // The emergency door. // // When `nodd_declare` was broken, this gate had no way out: it refused every // write until `.nodd//feature.md` existed, and the only tool that could // create it was the broken one. Writing the doc by hand hit the artifact // branch; disabling the gate meant writing config, which is itself a mutation; // `/nodd-allow` does not exist for a subagent. Five closed paths, verified — // the gate blocked every repair of its own cause. // // Creating the declared document is therefore allowed. It is faithful to the // gate's intent: the artifact branch exists so the model cannot *forge* a doc // after the fact, not so the doc cannot come into being. // --------------------------------------------------------------------------- test("the declared feature doc may be created when it does not exist yet", () => { const doc = { toolName: "write", input: { file_path: `/repo/${featureDocRelPath("demo")}` } }; const decision = trackGate(routed("tracked"), doc, emptyPolicy(), absent); assert.equal(decision.allow, true, "the only way out of the deadlock must stay open"); }); test("once the doc exists it is NODD's again", () => { // The door closes behind itself: with the document in place, the model has // no business rewriting it, which is the forgery the gate exists to stop. const doc = { toolName: "write", input: { file_path: `/repo/${featureDocRelPath("demo")}` } }; const decision = trackGate(routed("tracked"), doc, emptyPolicy(), present); assert.equal(decision.allow, false, "an existing feature doc is extension-owned"); }); test("the escape does not extend to another slug's document", () => { const other = { toolName: "write", input: { file_path: `/repo/${featureDocRelPath("other")}` } }; const decision = trackGate(routed("tracked"), other, emptyPolicy(), absent); assert.equal(decision.allow, false, "only the declared slug's own doc is exempt"); }); test("the escape does not extend to other NODD artifacts", () => { // The ledger is evidence. Nothing about the deadlock requires opening it. const ledger = { toolName: "write", input: { file_path: "/repo/.nodd/demo/ledger.json" } }; const decision = trackGate(routed("tracked"), ledger, emptyPolicy(), absent); assert.equal(decision.allow, false, "the ledger stays extension-owned"); }); test("mutating bash aimed at .nodd/ is refused like a write, whatever the route", () => { for (const command of [ "sed -i 's/- \\[ \\]/- [x]/' .nodd/demo/feature.md", "echo '{}' > .nodd/demo/ledger.json", "rm -rf .nodd", "cp /tmp/fake.md /repo/.nodd/demo/feature.md", ]) { const decision = trackGate(routed("inline"), { toolName: "bash", input: { command } }, emptyPolicy(), present); assert.equal(decision.allow, false, command); assert.ok(decision.allow === false && decision.remedy.action.includes("nodd_task"), command); } }); test("reading .nodd/ through bash is still a read", () => { for (const command of ["cat .nodd/demo/feature.md", "grep -r T1 .nodd/", "ls .nodd"]) { assert.equal(trackGate(routed("tracked"), { toolName: "bash", input: { command } }, emptyPolicy(), present).allow, true, command); } }); test("a mutating command that never names .nodd is not this gate's business", () => { assert.equal(trackGate(routed("tracked"), { toolName: "bash", input: { command: "echo x > /repo/projects/nodd/out.txt" } }, emptyPolicy(), present).allow, true); });