// Row 101 (#1595): a privileged add names its gate owner. The refusal is a // write-time check in store.mjs, so it is exercised through the CLI in a // scratch repository. Replay is exercised through loadDoc, which runs the real // replay path over a log that holds an old privileged add with no gate owner. import assert from "node:assert/strict"; import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { test } from "node:test"; import { applyEntry, buildDoc, canonArgs, genesisReceipt, genesisRows, loadDoc, logDigest, parseMigrationMap, render, rowsArray, serialize, sha256, splitView, } from "../src/queue.mjs"; import { QueueError } from "../src/errors.mjs"; import { MAP_ROWS, cli, genesisCommitted, mapText, scratchRepo } from "./helpers.mjs"; const ROOT = "/srv/repo"; const BLOB = "a".repeat(40); const BLOB2 = "b".repeat(40); let clock = 0; const at = () => new Date(Date.UTC(2026, 9, 13, 12, 0, clock++)).toISOString(); function brief() { return { path: "docs/plans/brief-b.md", anchor: "Queue", blob: BLOB }; } function addArgs(extra = {}) { return { piece: "Gate row", gate: "g", brief: "docs/plans/brief-b.md#Queue", ...extra }; } // A genesis document built the way store.mjs builds one. function genesisDoc() { const map = parseMigrationMap(mapText(MAP_ROWS)); const blobs = new Map(map.rows.filter((r) => r.brief).map((r) => [r.id, BLOB])); const t = at(); const grows = genesisRows(map, blobs, "genesis-gate-0001", t, "sage"); const entry = { rev: 0, op: "genesis-gate-0001", verb: "genesis", args: { root: ROOT, branch: "refactor", map: "agents/sage/work/queue-migration-map.md" }, by: "sage", at: t, semantics: 1, result: { mapBlob: BLOB2, highWater: map.highWater, retired: map.retired, rows: grows, legacyView: "legacy\n", receipt: genesisReceipt("genesis-gate-0001", grows.length) }, viewSha: sha256(render(grows, 0)), }; return { version: 1, canonicalRoot: ROOT, revision: 0, rows: grows, log: [entry] }; } // Appends one add at the given semantics, the way store.mjs would have written // it before this row: applyEntry only, no write-time check. function addUnder(doc, by, args, semantics) { const { state } = loadDoc(Buffer.from(serialize(doc))); const rev = doc.revision + 1; const entry = { rev, op: `add-gate-${rev}`, verb: "add", args: canonArgs("add", args), by, at: at(), semantics, result: null, viewSha: null }; const out = applyEntry(state, entry, { brief: brief() }); entry.result = out.result; entry.viewSha = sha256(render(rowsArray(out.state), rev)); return buildDoc(doc.canonicalRoot, out.state, [...doc.log, entry]); } test("an old privileged add with no gate owner still replays through loadDoc, and gets jason", () => { for (const semantics of [1, 2]) { const d = addUnder(genesisDoc(), "sage", addArgs(), semantics); const { doc, state } = loadDoc(Buffer.from(serialize(d))); assert.equal(doc.rows.at(-1).gateOwner, "jason"); assert.equal(state.rows.get(doc.rows.at(-1).id).gateOwner, "jason"); } }); test("a non-privileged add gets jason through the replay path and can't name a gate owner", () => { const d = addUnder(genesisDoc(), "dewey", addArgs(), 2); assert.equal(loadDoc(Buffer.from(serialize(d))).doc.rows.at(-1).gateOwner, "jason"); assert.throws( () => addUnder(genesisDoc(), "dewey", addArgs({ gateOwner: "filbert" }), 2), (err) => err instanceof QueueError && err.code === 2 && /only a privileged actor may set gateOwner on add/.test(err.message), ); }); test("the CLI refuses a privileged add without --gate-owner, for jason and sage, and writes nothing", (t) => { const repo = scratchRepo(t); genesisCommitted(repo); const queueBefore = readFileSync(repo.queuePath); const viewBefore = readFileSync(repo.viewPath); const revBefore = JSON.parse(queueBefore.toString("utf8")).revision; const args = ["add", "--op", "add-gate-nogate-1", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Template"]; for (const by of ["sage", "jason"]) { const r = cli(repo, args, { by }); assert.equal(r.code, 2, r.err); assert.match(r.err, /--gate-owner/); assert.match(r.err, /no default gate owner/); assert.doesNotMatch(r.err, /row \d+/, "the refusal states the rule, not a row's history"); assert.deepEqual(readFileSync(repo.queuePath), queueBefore); assert.deepEqual(readFileSync(repo.viewPath), viewBefore); assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, revBefore); } }); test("the CLI add with --gate-owner carries the named owner and records a log entry", (t) => { const repo = scratchRepo(t); genesisCommitted(repo); const r = cli(repo, ["add", "--op", "add-gate-named-1", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Template", "--gate-owner", "filbert"], { by: "sage" }); assert.equal(r.code, 0, r.err); const d = JSON.parse(readFileSync(repo.queuePath, "utf8")); assert.equal(d.rows.at(-1).gateOwner, "filbert"); assert.equal(d.log.at(-1).op, "add-gate-named-1"); assert.equal(d.log.at(-1).by, "sage"); }); test("the CLI add by a non-privileged seat gets jason and can't name a gate owner", (t) => { const repo = scratchRepo(t); genesisCommitted(repo); const base = ["--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Template"]; const ok = cli(repo, ["add", "--op", "add-gate-dewey-1", ...base], { by: "dewey" }); assert.equal(ok.code, 0, ok.err); assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).rows.at(-1).gateOwner, "jason"); const before = readFileSync(repo.queuePath); const refusedRun = cli(repo, ["add", "--op", "add-gate-dewey-2", ...base, "--gate-owner", "filbert"], { by: "dewey" }); assert.equal(refusedRun.code, 2, refusedRun.err); assert.match(refusedRun.err, /only a privileged actor may set gateOwner on add/); assert.deepEqual(readFileSync(repo.queuePath), before); }); // Step 3 of the write path answers a recorded op before the gate-owner check // runs. An add recorded before row 101 has a null gate owner, so a retry of it // carries none either; hoisting the check above step 3 would refuse that retry. // Every other retry test passes --gate-owner, so this one pins the order. test("a retried sage add recorded with no gate owner answers (already recorded at rev N) and writes nothing", (t) => { const repo = scratchRepo(t); genesisCommitted(repo); const before = JSON.parse(readFileSync(repo.queuePath, "utf8")); const doc = addUnder(before, "sage", addArgs(), 2); const entry = doc.log.at(-1); assert.equal(entry.args.gateOwner, null); assert.equal(entry.by, "sage"); // The state the old write path left: queue.json, the view and the witness all at the add's rev. const bytes = Buffer.from(serialize(doc)); const parts = splitView(readFileSync(repo.viewPath, "utf8")); writeFileSync(repo.queuePath, bytes); writeFileSync(repo.viewPath, parts.head + render(doc.rows, entry.rev) + parts.tail); writeFileSync(join(repo.gitDir, "mosaic-queue.head"), `${JSON.stringify({ revision: entry.rev, logDigest: logDigest(doc.log, entry.rev), fileSha: sha256(bytes), at: entry.at })}\n`); const queueBefore = readFileSync(repo.queuePath); const viewBefore = readFileSync(repo.viewPath); const retry = ["add", "--op", entry.op, "--piece", "Gate row", "--gate", "g", "--brief", "docs/plans/brief-b.md#Queue"]; const r = cli(repo, retry, { by: "sage" }); assert.equal(r.code, 0, r.err); assert.match(r.out, new RegExp(`\\(already recorded at rev ${entry.rev}\\)`)); assert.deepEqual(readFileSync(repo.queuePath), queueBefore); assert.deepEqual(readFileSync(repo.viewPath), viewBefore); assert.equal(JSON.parse(readFileSync(repo.queuePath, "utf8")).revision, entry.rev); // The check still bites a new op with the same arguments. const fresh = cli(repo, ["add", "--op", "add-gate-fresh-1", ...retry.slice(3)], { by: "sage" }); assert.equal(fresh.code, 2, fresh.err); assert.match(fresh.err, /--gate-owner/); assert.deepEqual(readFileSync(repo.queuePath), queueBefore); });