Filbert approved round 1 (f167b85e). Manifest 782bcb62, 21 files, plus the QUEUE.md markers and the TOOLS.md section. Lead decision 35. Co-Authored-By: Claude Opus 5.5 <[email protected]>
643 lines
38 KiB
JavaScript
643 lines
38 KiB
JavaScript
// The pure layer: schema, serialization, the 8.7 matrix, replay, render,
|
||
// view classification and `next` (8.8). No file or git access.
|
||
import assert from "node:assert/strict";
|
||
import { test } from "node:test";
|
||
import {
|
||
CALLER_OP_RE, LOG_OP_RE, STATES, applyEntry, buildDoc, canonArgs, classifyView, countHeading, genesisReceipt, genesisRows,
|
||
gitBlobId, loadDoc, nextFor, parseManifest, parseMigrationMap, render, rowsArray, serialize, sha256, splitView, validateRow,
|
||
} from "../src/queue.mjs";
|
||
import { QueueError } from "../src/errors.mjs";
|
||
import { MAP_ROWS, mapText } 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, 8, 26, 12, 0, clock++)).toISOString();
|
||
|
||
function brief(path = "docs/plans/brief-b.md", anchor = "Queue", blob = BLOB) {
|
||
return { path, anchor, blob };
|
||
}
|
||
|
||
// A genesis document built the way store.mjs builds one.
|
||
function genesisDoc(rows = MAP_ROWS, op = "genesis-op-0001") {
|
||
const map = parseMigrationMap(mapText(rows));
|
||
const blobs = new Map(map.rows.filter((r) => r.brief).map((r) => [r.id, BLOB]));
|
||
const t = at();
|
||
const grows = genesisRows(map, blobs, op, t, "sage");
|
||
const body = render(grows, 0);
|
||
const entry = {
|
||
rev: 0, op, 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(op, grows.length) },
|
||
viewSha: sha256(body),
|
||
};
|
||
return { version: 1, canonicalRoot: ROOT, revision: 0, rows: grows, log: [entry] };
|
||
}
|
||
|
||
// Appends one op the way store.mjs does and returns the new document.
|
||
function step(doc, verb, args, by, resolved = {}, op = `op-${verb}-${doc.revision + 1}-xxxx`) {
|
||
const { state } = loadDoc(Buffer.from(serialize(doc)));
|
||
const entry = { rev: doc.revision + 1, op, verb, args: canonArgs(verb, args), by, at: at(), semantics: 1, result: null, viewSha: null };
|
||
const out = applyEntry(state, entry, resolved);
|
||
entry.result = out.result;
|
||
entry.viewSha = sha256(render(rowsArray(out.state), entry.rev));
|
||
return buildDoc(doc.canonicalRoot, out.state, [...doc.log, entry]);
|
||
}
|
||
|
||
function refused(fn, re) {
|
||
assert.throws(fn, (err) => err instanceof QueueError && err.code === 2 && re.test(err.message));
|
||
}
|
||
|
||
const mv = (id, to, extra = {}) => ({ id, to, reason: null, candidate: null, evidence: null, issue: null, ...extra });
|
||
const row = (doc, id) => doc.rows.find((r) => r.id === id);
|
||
const MANIFEST = `${"c".repeat(64)} packages/queue/src/queue.mjs\n`;
|
||
const CAND = { kind: "manifest", digest: sha256(MANIFEST), text: MANIFEST };
|
||
|
||
// Row 9 in progress (row 6 blocked first, since row 9 waits on 6 settled).
|
||
function row9Started() {
|
||
let d = genesisDoc();
|
||
d = step(d, "move", mv(6, "blocked", { reason: "paused" }), "darkwing");
|
||
return step(d, "move", mv(9, "in-progress"), "darkwing");
|
||
}
|
||
|
||
test("genesis document serializes deterministically and replays", () => {
|
||
const doc = genesisDoc();
|
||
const text = serialize(doc);
|
||
assert.equal(serialize(JSON.parse(text)), text);
|
||
assert.ok(text.endsWith("}\n"));
|
||
const loaded = loadDoc(Buffer.from(text));
|
||
assert.equal(loaded.branch, "refactor");
|
||
assert.deepEqual(rowsArray(loaded.state), doc.rows);
|
||
assert.deepEqual(row(doc, 6).claim, { seat: "darkwing", op: "genesis-op-0001" });
|
||
assert.equal(row(doc, 9).claim, null);
|
||
});
|
||
|
||
test("a hand edit that stays valid JSON fails replay; a formatting-only edit fails re-serialization", () => {
|
||
const doc = step(genesisDoc(), "note", { id: 9, text: "hello" }, "darkwing");
|
||
const edited = JSON.parse(serialize(doc));
|
||
edited.rows.find((r) => r.id === 9).note = "changed by hand";
|
||
refused(() => loadDoc(Buffer.from(serialize(edited))), /do not equal the replay/);
|
||
const loose = serialize(doc).replace('"revision": 1', '"revision": 1');
|
||
refused(() => loadDoc(Buffer.from(loose)), /re-serialize byte for byte/);
|
||
const reordered = serialize(doc).replace(/"version": 1,\n "canonicalRoot": "[^"]*",/, `"canonicalRoot": "${ROOT}",\n "version": 1,`);
|
||
refused(() => loadDoc(Buffer.from(reordered)), /keys must be exactly/);
|
||
});
|
||
|
||
test("a tampered result, receipt or viewSha fails replay", () => {
|
||
const doc = step(genesisDoc(), "note", { id: 9, text: "hello" }, "darkwing");
|
||
const a = JSON.parse(serialize(doc));
|
||
a.log[1].result.receipt = "ok forged";
|
||
refused(() => loadDoc(Buffer.from(serialize(a))), /result differs from its replay/);
|
||
const b = JSON.parse(serialize(doc));
|
||
b.log[1].viewSha = "0".repeat(64);
|
||
refused(() => loadDoc(Buffer.from(serialize(b))), /viewSha is not the render/);
|
||
const c = JSON.parse(serialize(doc));
|
||
c.log[1].by = "rocko";
|
||
refused(() => loadDoc(Buffer.from(serialize(c))), /does not replay/);
|
||
});
|
||
|
||
test("op ids: 8 to 72 characters for callers, 80 in the log for .outcome entries", () => {
|
||
assert.ok(CALLER_OP_RE.test("a".repeat(72)));
|
||
assert.ok(!CALLER_OP_RE.test("a".repeat(73)));
|
||
assert.ok(!CALLER_OP_RE.test("short"));
|
||
assert.ok(!CALLER_OP_RE.test("-leading-dash"));
|
||
assert.ok(LOG_OP_RE.test(`${"a".repeat(72)}.outcome`));
|
||
assert.ok(!LOG_OP_RE.test(`${"a".repeat(73)}.outcome`));
|
||
});
|
||
|
||
test("add: defaults for an ordinary seat, privileged extras, refusals", () => {
|
||
const d = genesisDoc();
|
||
const add = { piece: "New", gate: "tests pass", brief: "docs/plans/brief-b.md#Queue", issues: [1508] };
|
||
const d1 = step(d, "add", add, "dewey", { brief: brief() });
|
||
const r = row(d1, 12);
|
||
assert.equal(r.owner, "dewey");
|
||
assert.equal(r.state, "queued");
|
||
assert.deepEqual(r.closes, [1508]);
|
||
assert.equal(r.gateOwner, "jason");
|
||
assert.deepEqual([r.after, r.reviewers, r.required, r.claim], [[], [], false, null]);
|
||
assert.match(d1.log[1].result.receipt, /row 12 none→queued$/);
|
||
refused(() => step(d, "add", { ...add, owner: "rocko" }, "dewey", { brief: brief() }), /only a privileged actor may set owner/);
|
||
refused(() => step(d, "add", { ...add, reviewers: ["rocko"] }, "dewey", { brief: brief() }), /reviewers/);
|
||
refused(() => step(d, "add", { ...add, required: true }, "dewey", { brief: brief() }), /required/);
|
||
const d2 = step(d, "add", { ...add, owner: "rocko", gateOwner: "filbert", after: [{ id: 9, when: "done" }], reviewers: ["dewey"], required: true }, "sage", { brief: brief() });
|
||
const r2 = row(d2, 12);
|
||
assert.deepEqual([r2.owner, r2.gateOwner, r2.required, r2.after], ["rocko", "filbert", true, [{ id: 9, when: "done" }]]);
|
||
refused(() => step(d, "add", { ...add, after: [{ id: 99, when: "done" }] }, "sage", { brief: brief() }), /missing row 99/);
|
||
// ids come from the high-water mark, so a retired id is never reused.
|
||
const d3 = step(d2, "add", add, "dewey", { brief: brief() });
|
||
assert.ok(row(d3, 13));
|
||
});
|
||
|
||
test("matrix: queued→briefed privileged; briefed→in-progress owner with after satisfied", () => {
|
||
let d = step(genesisDoc(), "add", { piece: "N", gate: "g", brief: "docs/plans/brief-b.md#Queue" }, "dewey", { brief: brief() });
|
||
refused(() => step(d, "move", mv(12, "briefed"), "dewey"), /privileged/);
|
||
d = step(d, "move", mv(12, "briefed"), "sage");
|
||
refused(() => step(d, "move", mv(12, "in-progress"), "sage"), /only the owner \(dewey\) may start/);
|
||
d = step(d, "move", mv(12, "in-progress"), "dewey");
|
||
assert.deepEqual(row(d, 12).claim.seat, "dewey");
|
||
refused(() => step(d, "move", mv(12, "in-progress"), "dewey"), /not a transition/);
|
||
// row 9 waits on row 6 settled; row 6 is in progress at genesis.
|
||
refused(() => step(genesisDoc(), "move", mv(9, "in-progress"), "darkwing"), /waits on 6 \(settled; now in-progress\)/);
|
||
const d9 = row9Started();
|
||
assert.equal(row(d9, 9).state, "in-progress");
|
||
});
|
||
|
||
test("matrix: release, review round, changes requested and waiting-on-jason", () => {
|
||
let d = row9Started();
|
||
refused(() => step(d, "move", mv(9, "briefed"), "darkwing"), /use `queue release 9`/);
|
||
refused(() => step(d, "release", { id: 9 }, "dewey"), /only the claimant/);
|
||
const released = step(d, "release", { id: 9 }, "darkwing");
|
||
assert.deepEqual([row(released, 9).state, row(released, 9).claim], ["briefed", null]);
|
||
step(d, "release", { id: 9 }, "sage");
|
||
refused(() => step(d, "move", mv(9, "in-review"), "darkwing"), /needs --candidate/);
|
||
refused(() => step(d, "move", mv(9, "in-review", { candidate: "x.sha256" }), "dewey", { candidate: CAND }), /only the claimant/);
|
||
d = step(d, "move", mv(9, "in-review", { candidate: "x.sha256" }), "darkwing", { candidate: CAND });
|
||
const rv = row(d, 9).review;
|
||
assert.deepEqual(Object.keys(rv), ["rounds"]);
|
||
assert.deepEqual(rv.rounds.map((r) => [r.n, r.issue, r.request, r.candidate.digest]), [[1, 1508, "none", CAND.digest]]);
|
||
assert.match(d.log.at(-1).result.receipt, /in-progress→in-review round 1 on #1508$/);
|
||
refused(() => step(d, "move", mv(9, "in-progress"), "dewey"), /claimed by darkwing/);
|
||
d = step(d, "move", mv(9, "in-progress"), "darkwing");
|
||
assert.equal(row(d, 9).claim.seat, "darkwing");
|
||
d = step(d, "move", mv(9, "in-review", { candidate: "y" }), "darkwing", { candidate: { kind: "commit", digest: BLOB2, text: null } });
|
||
assert.equal(row(d, 9).review.rounds.length, 2);
|
||
const w = step(d, "move", mv(9, "waiting-on-jason"), "sage");
|
||
assert.equal(row(w, 9).claim.seat, "darkwing");
|
||
refused(() => step(w, "move", mv(9, "done"), "sage"), /sage closes a waiting-on-jason row only with --evidence/);
|
||
refused(() => step(w, "move", mv(9, "done"), "darkwing"), /only jason/);
|
||
const done = step(w, "move", mv(9, "done", { evidence: "Jason approved in thread X" }), "sage");
|
||
assert.deepEqual([row(done, 9).state, row(done, 9).claim], ["done", null]);
|
||
const doneJ = step(w, "move", mv(9, "done"), "jason");
|
||
refused(() => step(doneJ, "move", mv(9, "blocked", { reason: "x" }), "jason"), /done rows never change/);
|
||
refused(() => step(doneJ, "note", { id: 9, text: "x" }, "jason"), /notes are closed/);
|
||
});
|
||
|
||
test("matrix J5: in-review→done by the gate owner with evidence naming the current round", () => {
|
||
let d = row9Started();
|
||
d = step(d, "move", mv(9, "in-review", { candidate: "x" }), "darkwing", { candidate: CAND });
|
||
const ev = `comment=4242,round=1,candidate=${CAND.digest}`;
|
||
refused(() => step(d, "move", mv(9, "done"), "filbert"), /--evidence comment=<id>,round=<n>,candidate=<digest>/);
|
||
refused(() => step(d, "move", mv(9, "done", { evidence: `comment=4242,candidate=${CAND.digest}` }), "filbert"), /round=<n>/);
|
||
refused(() => step(d, "move", mv(9, "done", { evidence: `comment=1,round=1,candidate=${"d".repeat(64)}` }), "filbert"), /is not round 1's candidate/);
|
||
refused(() => step(d, "move", mv(9, "done", { evidence: `comment=4242,round=2,candidate=${CAND.digest}` }), "filbert"), /evidence names round 2; row 9 is in round 1/);
|
||
refused(() => step(d, "move", mv(9, "done", { evidence: ev }), "rocko"), /only the gate owner \(filbert\)/);
|
||
const done = step(d, "move", mv(9, "done", { evidence: ev }), "filbert");
|
||
assert.equal(row(done, 9).state, "done");
|
||
step(d, "move", mv(9, "done", { evidence: ev }), "sage");
|
||
// Row 11's gate is Jason's: in-review→done is refused for everyone.
|
||
let e = step(genesisDoc(), "move", mv(11, "in-progress"), "dewey");
|
||
e = step(e, "move", mv(11, "in-review", { candidate: "x" }), "dewey", { candidate: CAND });
|
||
refused(() => step(e, "move", mv(11, "done", { evidence: ev }), "jason"), /gate is Jason's/);
|
||
// Changes requested, then the same candidate again: round 1's comment
|
||
// does not close round 2 (8.7, R3).
|
||
d = step(d, "move", mv(9, "in-progress"), "darkwing");
|
||
d = step(d, "move", mv(9, "in-review", { candidate: "x" }), "darkwing", { candidate: CAND });
|
||
assert.deepEqual(row(d, 9).review.rounds.map((r) => r.candidate.digest), [CAND.digest, CAND.digest]);
|
||
refused(() => step(d, "move", mv(9, "done", { evidence: ev }), "filbert"), /evidence names round 1; row 9 is in round 2/);
|
||
const done2 = step(d, "move", mv(9, "done", { evidence: `comment=4343,round=2,candidate=${CAND.digest}` }), "filbert");
|
||
assert.equal(done2.log.at(-1).result.round, 2);
|
||
});
|
||
|
||
// A row 12 owned by darkwing with the given issues, started, so the next
|
||
// move is the review request.
|
||
function row12Started(issues, { gateOwner = "filbert" } = {}) {
|
||
let d = step(genesisDoc(), "add", { piece: "N", gate: "g", brief: "docs/plans/brief-b.md#Queue", issues, owner: "darkwing", gateOwner }, "sage", { brief: brief() });
|
||
d = step(d, "move", mv(12, "briefed"), "sage");
|
||
return step(d, "move", mv(12, "in-progress"), "darkwing");
|
||
}
|
||
|
||
const review = (d, extra = {}) => step(d, "move", mv(12, "in-review", { candidate: "x", ...extra }), "darkwing", { candidate: CAND });
|
||
const again = (d) => step(d, "move", mv(12, "in-progress"), "darkwing");
|
||
// The issue the next round keeps: the last round's own (P2).
|
||
const kept = (d) => row(d, 12).review.rounds.at(-1).issue;
|
||
|
||
test("review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it", () => {
|
||
// No issues: refused before the round opens.
|
||
refused(() => review(row12Started([])), /row 12 lists no issues; a privileged actor sets one before review/);
|
||
// One issue: used without --issue; --issue may name it; any other refuses.
|
||
const one = review(row12Started([1508]));
|
||
assert.equal(kept(one), 1508);
|
||
assert.match(one.log.at(-1).result.receipt, /round 1 on #1508$/);
|
||
assert.equal(one.log.at(-1).result.issue, 1508);
|
||
refused(() => review(row12Started([1508]), { issue: 1495 }), /--issue #1495 is not one of row 12's issues \(#1508\)/);
|
||
// Several: --issue is required and must be one of the row's; the lowest
|
||
// number is not a default.
|
||
const several = row12Started([1495, 1508]);
|
||
refused(() => review(several), /row 12 lists several issues \(#1495, #1508\); name the review's issue with --issue/);
|
||
refused(() => review(several, { issue: 1600 }), /--issue #1600 is not one of row 12's issues \(#1495, #1508\)/);
|
||
let d = review(several, { issue: 1508 });
|
||
assert.equal(kept(d), 1508);
|
||
// Later rounds keep the previous round's issue unless --issue names another.
|
||
d = review(again(d));
|
||
assert.deepEqual([kept(d), row(d, 12).review.rounds.length], [1508, 2]);
|
||
d = review(again(d), { issue: 1495 });
|
||
assert.deepEqual([kept(d), row(d, 12).review.rounds.length], [1495, 3]);
|
||
// P2: each round keeps the issue it posted to; a later --issue rewrites none.
|
||
assert.deepEqual(row(d, 12).review.rounds.map((r) => [r.n, r.issue]), [[1, 1508], [2, 1508], [3, 1495]]);
|
||
d = review(again(d));
|
||
assert.deepEqual(row(d, 12).review.rounds.map((r) => r.issue), [1508, 1508, 1495, 1495]);
|
||
// A kept issue the row no longer lists refuses until --issue names one.
|
||
d = step(again(d), "set", { id: 12, field: "issues", value: [1508, 1600] }, "sage");
|
||
refused(() => review(d), /review issue #1495 is no longer one of its issues \(#1508, #1600\); name one with --issue/);
|
||
assert.equal(kept(review(d, { issue: 1600 })), 1600);
|
||
// --issue belongs to the review request only.
|
||
refused(() => step(several, "move", mv(12, "blocked", { reason: "x", issue: 1508 }), "darkwing"), /--issue applies only to in-progress→in-review/);
|
||
});
|
||
|
||
test("the row schema refuses a round with a null issue, and the A1 review shape (P2)", () => {
|
||
const r = structuredClone(row(review(row12Started([1508])), 12));
|
||
validateRow(r);
|
||
const bad = structuredClone(r);
|
||
bad.review.rounds[0].issue = null;
|
||
refused(() => validateRow(bad), /review round issue must be a positive integer/);
|
||
const noIssue = structuredClone(r);
|
||
delete noIssue.review.rounds[0].issue;
|
||
refused(() => validateRow(noIssue), /review round keys must be exactly n, op, by, at, issue, candidate, request/);
|
||
const a1 = structuredClone(r);
|
||
a1.review = { issue: 1508, rounds: a1.review.rounds };
|
||
refused(() => validateRow(a1), /review keys must be exactly rounds in that order/);
|
||
});
|
||
|
||
// R1: every state × target × actor class against 8.7's table, written from
|
||
// the spec rather than from queue.mjs. Row 12 is owned by darkwing; the
|
||
// gate owner is filbert, jason or the owner; rocko is any other seat.
|
||
const ACTORS = ["darkwing", "filbert", "rocko", "sage", "jason"];
|
||
const PRIV = new Set(["sage", "jason"]);
|
||
|
||
function specAllows({ from, prev, to, by, gateOwner, required }) {
|
||
const own = by === "darkwing";
|
||
const priv = PRIV.has(by);
|
||
if (from === "done") return false;
|
||
if (to === "blocked") return ["queued", "briefed", "in-progress", "in-review", "waiting-on-jason"].includes(from) && (own || priv);
|
||
if (from === "blocked") return to === prev && (own || priv);
|
||
const edge = `${from}→${to}`;
|
||
switch (edge) {
|
||
case "queued→briefed": return priv;
|
||
case "briefed→in-progress": return own;
|
||
case "in-progress→in-review": return own; // the claimant is the owner
|
||
case "in-review→in-progress": case "in-review→waiting-on-jason": return own || priv;
|
||
case "waiting-on-jason→done": return by === "jason" || by === "sage"; // sage with evidence, which the case supplies
|
||
case "in-review→done": return gateOwner !== "jason" && (by === gateOwner || priv);
|
||
case "queued→parked": case "briefed→parked": return by === "jason" && !required;
|
||
case "parked→queued": return by === "jason";
|
||
default: return false; // in-progress→briefed is `release`, not `move`
|
||
}
|
||
}
|
||
|
||
function matrixStates(gateOwner, required) {
|
||
let q = step(genesisDoc(), "add", {
|
||
piece: "M", gate: "g", brief: "docs/plans/brief-b.md#Queue", issues: [1508], owner: "darkwing", gateOwner, required,
|
||
}, "sage", { brief: brief() });
|
||
const out = [{ from: "queued", doc: q }];
|
||
if (!required) out.push({ from: "parked", doc: step(q, "move", mv(12, "parked"), "jason") });
|
||
const b = step(q, "move", mv(12, "briefed"), "sage");
|
||
const p = step(b, "move", mv(12, "in-progress"), "darkwing");
|
||
const r = review(p);
|
||
const w = step(r, "move", mv(12, "waiting-on-jason"), "sage");
|
||
out.push({ from: "briefed", doc: b }, { from: "in-progress", doc: p }, { from: "in-review", doc: r }, { from: "waiting-on-jason", doc: w });
|
||
out.push({ from: "done", doc: step(w, "move", mv(12, "done"), "jason") });
|
||
for (const s of [...out]) {
|
||
if (!["done", "parked"].includes(s.from)) out.push({ from: "blocked", prev: s.from, doc: step(s.doc, "move", mv(12, "blocked", { reason: "r" }), "sage") });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function matrixArgs(from, to) {
|
||
const extra = {};
|
||
if (to === "blocked") extra.reason = "r";
|
||
if (from === "in-progress" && to === "in-review") extra.candidate = "x";
|
||
if (to === "done" && from === "in-review") extra.evidence = `comment=1,round=1,candidate=${CAND.digest}`;
|
||
if (to === "done" && from === "waiting-on-jason") extra.evidence = "Jason approved in thread X";
|
||
return mv(12, to, extra);
|
||
}
|
||
|
||
test("matrix R1: every state × target × actor class matches 8.7, gate owner jason or not, required or not", () => {
|
||
let allowed = 0;
|
||
let refusals = 0;
|
||
for (const gateOwner of ["filbert", "jason", "darkwing"]) {
|
||
for (const required of [false, true]) {
|
||
for (const { from, prev = null, doc } of matrixStates(gateOwner, required)) {
|
||
assert.equal(row(doc, 12).state, from);
|
||
for (const to of STATES) {
|
||
for (const by of ACTORS) {
|
||
const c = { from, prev, to, by, gateOwner, required };
|
||
const label = JSON.stringify(c);
|
||
let got;
|
||
try {
|
||
got = step(doc, "move", matrixArgs(from, to), by, { candidate: CAND });
|
||
} catch (err) {
|
||
assert.ok(err instanceof QueueError && err.code === 2, `${label}: ${err.stack}`);
|
||
assert.equal(specAllows(c), false, `${label} refused: ${err.message}`);
|
||
refusals++;
|
||
continue;
|
||
}
|
||
assert.equal(specAllows(c), true, `${label} was allowed`);
|
||
const r = row(got, 12);
|
||
assert.equal(r.state, to, label);
|
||
if (to === "blocked") assert.equal(r.previousState, from, label);
|
||
if (from === "blocked") assert.deepEqual([r.previousState, r.blockedReason], [null, null], label);
|
||
allowed++;
|
||
}
|
||
}
|
||
// release: the claimant or a privileged actor, from in-progress only.
|
||
for (const by of ACTORS) {
|
||
const ok = from === "in-progress" && (by === "darkwing" || PRIV.has(by));
|
||
const label = JSON.stringify({ release: from, by, gateOwner, required });
|
||
if (ok) assert.deepEqual([row(step(doc, "release", { id: 12 }, by), 12).state], ["briefed"], label);
|
||
else assert.throws(() => step(doc, "release", { id: 12 }, by), (err) => err instanceof QueueError && err.code === 2, label);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assert.ok(allowed > 100 && refusals > 1000, `allowed ${allowed}, refused ${refusals}`);
|
||
});
|
||
|
||
test("matrix: blocked keeps the claim and returns only to previousState", () => {
|
||
let d = row9Started();
|
||
refused(() => step(d, "move", mv(9, "blocked"), "darkwing"), /needs --reason/);
|
||
refused(() => step(d, "move", mv(9, "blocked", { reason: "x" }), "rocko"), /claimed by darkwing/);
|
||
d = step(d, "move", mv(9, "blocked", { reason: "waiting on git" }), "darkwing");
|
||
const r = row(d, 9);
|
||
assert.deepEqual([r.state, r.previousState, r.blockedReason, r.claim.seat], ["blocked", "in-progress", "waiting on git", "darkwing"]);
|
||
refused(() => step(d, "move", mv(9, "blocked", { reason: "y" }), "darkwing"), /already blocked; update the reason with note/);
|
||
const noted = step(d, "note", { id: 9, text: "now waiting on review" }, "darkwing");
|
||
assert.equal(row(noted, 9).blockedReason, "now waiting on review");
|
||
refused(() => step(d, "note", { id: 9, text: "" }, "darkwing"), /reason cannot be empty/);
|
||
refused(() => step(d, "move", mv(9, "briefed"), "darkwing"), /returns only there/);
|
||
const back = step(d, "move", mv(9, "in-progress"), "sage");
|
||
assert.deepEqual([row(back, 9).state, row(back, 9).previousState, row(back, 9).blockedReason, row(back, 9).claim.seat], ["in-progress", null, null, "darkwing"]);
|
||
refused(() => step(genesisDoc(), "move", mv(1, "blocked", { reason: "x" }), "sage"), /done rows never change/);
|
||
refused(() => step(genesisDoc(), "move", mv(8, "blocked", { reason: "x" }), "jason"), /not a transition/);
|
||
});
|
||
|
||
test("matrix J4: parking is Jason's, refused while required; unpark returns to queued", () => {
|
||
const d = genesisDoc();
|
||
refused(() => step(d, "move", mv(11, "parked"), "sage"), /only jason may park/);
|
||
refused(() => step(d, "move", mv(9, "parked"), "jason"), /required and cannot be parked/);
|
||
const p = step(d, "move", mv(11, "parked"), "jason");
|
||
refused(() => step(p, "move", mv(11, "briefed"), "jason"), /not a transition/);
|
||
refused(() => step(p, "move", mv(11, "queued"), "sage"), /only jason may unpark/);
|
||
const u = step(p, "move", mv(11, "queued"), "jason");
|
||
assert.equal(row(u, 11).state, "queued");
|
||
refused(() => step(p, "set", { id: 11, field: "required", value: true }, "sage"), /parked; unpark it/);
|
||
refused(() => step(p, "note", { id: 11, text: "x" }, "jason"), /notes are closed/);
|
||
refused(() => step(row9Started(), "move", mv(9, "parked"), "jason"), /not a transition|required/);
|
||
});
|
||
|
||
test("field edits: who may change what", () => {
|
||
const d = genesisDoc();
|
||
const set = (id, field, value, reason = null) => ({ id, field, value, reason });
|
||
refused(() => step(d, "set", set(11, "piece", "x"), "dewey"), /privileged/);
|
||
assert.match(step(d, "set", set(11, "piece", "Renamed"), "sage").log.at(-1).result.receipt, /row 11 piece: Brief template→Renamed$/);
|
||
refused(() => step(d, "set", set(9, "after", []), "sage"), /only jason may change after on a required row/);
|
||
step(d, "set", set(9, "after", []), "jason");
|
||
step(d, "set", set(11, "after", [{ id: 9, when: "done" }]), "sage");
|
||
refused(() => step(d, "set", set(11, "after", [{ id: 11, when: "done" }]), "sage"), /lists itself/);
|
||
refused(() => step(d, "set", set(9, "required", false), "sage"), /only jason may clear required/);
|
||
const req = step(d, "set", set(11, "required", true), "sage");
|
||
assert.equal(row(req, 11).requiredSince, req.log.at(-1).at);
|
||
const cleared = step(req, "set", set(11, "required", false), "jason");
|
||
assert.equal(row(cleared, 11).requiredSince, null);
|
||
refused(() => step(d, "set", set(9, "closes", []), "sage"), /needs --reason/);
|
||
const narrowed = step(d, "set", set(9, "closes", [], "closed elsewhere"), "sage");
|
||
assert.deepEqual(row(narrowed, 9).closes, []);
|
||
refused(() => step(d, "set", set(9, "closes", [1]), "sage", {}), /--reason|not one of/);
|
||
refused(() => step(d, "set", set(9, "closes", [1], "r"), "sage"), /not one of row 9's issues/);
|
||
const issues = step(narrowed, "set", set(9, "issues", [1508, 1510]), "sage");
|
||
assert.deepEqual(row(issues, 9).closes, []);
|
||
refused(() => step(d, "set", set(9, "piece", "Queue as data"), "sage"), /already/);
|
||
refused(() => step(d, "set", set(1, "piece", "x"), "jason"), /done rows never change/);
|
||
const repin = step(d, "set", set(9, "brief", "docs/plans/brief-b.md#Queue"), "sage", { brief: brief(undefined, undefined, BLOB2) });
|
||
assert.equal(row(repin, 9).brief.blob, BLOB2);
|
||
refused(() => step(d, "set", set(9, "brief", "docs/plans/brief-b.md#Queue"), "sage", { brief: brief() }), /already pinned/);
|
||
refused(() => step(d, "set", set(9, "brief", "docs/plans/brief-b.md#Queue"), "darkwing", { brief: brief() }), /privileged/);
|
||
});
|
||
|
||
test("set issues keeps a logged narrowing of closes (N10)", () => {
|
||
const set = (id, field, value, reason = null) => ({ id, field, value, reason });
|
||
const last = (d) => d.log.at(-1).result;
|
||
// Not narrowed: closes follows the issues.
|
||
const wide = step(genesisDoc(), "set", set(9, "issues", [1508, 1510]), "sage");
|
||
assert.deepEqual(row(wide, 9).closes, [1508, 1510]);
|
||
assert.deepEqual(last(wide).closes, { from: [1508], to: [1508, 1510], narrowed: false });
|
||
assert.match(last(wide).receipt, /row 9 issues: #1508→#1508, #1510; closes: #1508→#1508, #1510$/);
|
||
// Narrowed: closes keeps its intersection with the new issues.
|
||
const narrowed = step(wide, "set", set(9, "closes", [1510], "1508 closes elsewhere"), "sage");
|
||
const more = step(narrowed, "set", set(9, "issues", [1508, 1510, 1600]), "sage");
|
||
assert.deepEqual(row(more, 9).closes, [1510]);
|
||
assert.deepEqual(last(more).closes, { from: [1510], to: [1510], narrowed: true });
|
||
assert.match(last(more).receipt, /; closes: #1510→#1510 \(kept narrowed\)$/);
|
||
const fewer = step(more, "set", set(9, "issues", [1508, 1600]), "sage");
|
||
assert.deepEqual(row(fewer, 9).closes, []);
|
||
assert.match(last(fewer).receipt, /; closes: #1510→none \(kept narrowed\)$/);
|
||
refused(() => step(narrowed, "set", set(9, "issues", [1508, 1510]), "sage"), /row 9 issues is already/);
|
||
// The results replay, and other fields carry no closes.
|
||
loadDoc(Buffer.from(serialize(fewer)));
|
||
assert.equal("closes" in last(step(genesisDoc(), "set", set(11, "piece", "Renamed"), "sage")), false);
|
||
});
|
||
|
||
// GFM's cell split: a backslash escapes the next character, and a pipe not
|
||
// escaped separates cells. The outer pipes are dropped first.
|
||
function gfmCells(line) {
|
||
const inner = line.trim().replace(/^\|/, "");
|
||
const cells = [""];
|
||
for (let i = 0; i < inner.length; i++) {
|
||
if (inner[i] === "\\" && i + 1 < inner.length) { cells[cells.length - 1] += inner[i] + inner[i + 1]; i++; continue; }
|
||
if (inner[i] === "|") cells.push("");
|
||
else cells[cells.length - 1] += inner[i];
|
||
}
|
||
if (cells.at(-1).trim() === "") cells.pop();
|
||
return cells.length;
|
||
}
|
||
|
||
test("text the table shows refuses \\ and <, everywhere it enters (N8)", () => {
|
||
const d = genesisDoc();
|
||
const bad = ["a\\| done | x", "back\\slash", "<b>bold</b>", "a < b"];
|
||
const set = (field, value) => ({ id: 11, field, value, reason: null });
|
||
for (const text of bad) {
|
||
const re = /must not contain \\ or </;
|
||
refused(() => canonArgs("add", { piece: text, gate: "g", brief: "docs/plans/brief-b.md#Queue" }), re);
|
||
refused(() => canonArgs("add", { piece: "p", gate: text, brief: "docs/plans/brief-b.md#Queue" }), re);
|
||
refused(() => canonArgs("add", { piece: "p", gate: "g", note: text, brief: "docs/plans/brief-b.md#Queue" }), re);
|
||
refused(() => canonArgs("add", { piece: "p", gate: "g", brief: `docs/plans/brief-b.md#${text}` }), re);
|
||
refused(() => canonArgs("move", mv(9, "blocked", { reason: text })), re);
|
||
refused(() => canonArgs("note", { id: 9, text }), re);
|
||
refused(() => canonArgs("set", set("piece", text)), re);
|
||
refused(() => canonArgs("set", set("gate", text)), re);
|
||
refused(() => canonArgs("set", set("brief", `docs/plans/brief-b.md#${text}`)), re);
|
||
// A hand-built row or log entry is refused the same way.
|
||
const r = row(d, 9);
|
||
for (const [k, v] of [["piece", text], ["gate", text], ["note", text], ["brief", { ...r.brief, anchor: text }]]) {
|
||
refused(() => validateRow({ ...r, [k]: v }), re);
|
||
}
|
||
refused(() => validateRow({ ...r, state: "blocked", previousState: "briefed", blockedReason: text }), re);
|
||
refused(() => loadDoc(Buffer.from(serialize(genesisDoc(MAP_ROWS.map((m) => (m.id === 9 ? { ...m, piece: text } : m)))))), re);
|
||
}
|
||
});
|
||
|
||
test("every accepted text renders to nine cells on every row (N8)", () => {
|
||
const base = row(genesisDoc(), 9);
|
||
const chars = [];
|
||
for (let c = 0x20; c < 0x7f; c++) chars.push(String.fromCharCode(c));
|
||
chars.push("é", "§", "—", String.fromCharCode(0xa0), "\u{1f600}");
|
||
const texts = ["a\\| done | x", "|", "||", "`|`", "a | b", "x\\", "\\|\\|", "<!-- x -->", "|"];
|
||
for (const c of chars) texts.push(`x${c}y`, `x${c}|${c}y`, `x${c}${c}|`);
|
||
const fields = [
|
||
(t) => ({ piece: t }), (t) => ({ gate: t }), (t) => ({ note: t }), (t) => ({ brief: { ...base.brief, anchor: t } }),
|
||
(t) => ({ state: "blocked", previousState: "briefed", blockedReason: t }),
|
||
];
|
||
const accepted = new Set();
|
||
for (const t of texts) {
|
||
for (const f of fields) {
|
||
const r = { ...base, ...f(t) };
|
||
try { validateRow(r); } catch (err) { if (err instanceof QueueError) continue; throw err; }
|
||
accepted.add(t);
|
||
const lines = render([r], 1).split("\n").filter((l) => l.startsWith("|"));
|
||
for (const l of lines) assert.equal(gfmCells(l), 9, `${JSON.stringify(t)} renders ${JSON.stringify(l)}`);
|
||
}
|
||
}
|
||
// Not vacuous: pipes and backticks pass; the two refused characters don't.
|
||
for (const t of ["|", "a | b", "`|`", "x|y"]) assert.ok(accepted.has(t), t);
|
||
for (const t of ["a\\| done | x", "x<y", "x\\y"]) assert.ok(!accepted.has(t), t);
|
||
});
|
||
|
||
test("replay holds every op id to the caller's rule (N11)", () => {
|
||
const load = (d) => loadDoc(Buffer.from(serialize(d)));
|
||
const d = genesisDoc();
|
||
// 72 characters replay; 73 to 80 no longer do.
|
||
load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "a".repeat(72)));
|
||
for (const n of [73, 80]) {
|
||
refused(() => load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "a".repeat(n))), /log entry 1 op .* must match/);
|
||
}
|
||
// .outcome is reserved on every verb, accept-history included.
|
||
refused(() => load(step(d, "accept-history", { reason: "r" }, "sage", { oldWitness: null }, "accept-history-1.outcome")), /log entry 1 op .* ends in \.outcome/);
|
||
refused(() => load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "note-row9.outcome")), /\.outcome, which is reserved/);
|
||
// The genesis op is checked too.
|
||
load(genesisDoc(MAP_ROWS, "genesis-ok-0001"));
|
||
for (const op of ["short", "Genesis-op-0001", "genesis-op-0001.outcome", "g".repeat(73)]) {
|
||
refused(() => load(genesisDoc(MAP_ROWS, op)), /log entry 0 op/);
|
||
}
|
||
// The ops a row carries, a claim and a review round, follow the same rule.
|
||
const claimed = structuredClone(row(row9Started(), 9));
|
||
const reviewed = structuredClone(row(review(row12Started([1508])), 12));
|
||
validateRow(claimed);
|
||
validateRow(reviewed);
|
||
for (const op of ["a".repeat(73), "start-row9.outcome"]) {
|
||
refused(() => validateRow({ ...claimed, claim: { ...claimed.claim, op } }), /claim op/);
|
||
const r = structuredClone(reviewed);
|
||
r.review.rounds[0].op = op;
|
||
refused(() => validateRow(r), /review round op/);
|
||
}
|
||
});
|
||
|
||
test("note: owner, listed reviewer or privileged; empty clears", () => {
|
||
const d = genesisDoc();
|
||
const n = step(d, "note", { id: 9, text: "from the reviewer" }, "filbert");
|
||
assert.equal(row(n, 9).note, "from the reviewer");
|
||
refused(() => step(d, "note", { id: 9, text: "x" }, "rocko"), /only the owner/);
|
||
assert.equal(row(step(n, "note", { id: 9, text: "" }, "darkwing"), 9).note, null);
|
||
refused(() => step(d, "note", { id: 9, text: "two\nlines" }, "darkwing"), /one line/);
|
||
refused(() => step(d, "note", { id: 9, text: `sep${String.fromCharCode(0x2028)}x` }, "darkwing"), /one line/);
|
||
});
|
||
|
||
test("assign moves the claim with the owner; done clears it", () => {
|
||
let d = row9Started();
|
||
refused(() => step(d, "assign", { id: 9, seat: "dewey" }, "darkwing"), /privileged/);
|
||
d = step(d, "assign", { id: 9, seat: "dewey" }, "sage", {}, "assign-row-9-dewey");
|
||
assert.deepEqual(row(d, 9).claim, { seat: "dewey", op: "assign-row-9-dewey" });
|
||
assert.match(d.log.at(-1).result.receipt, /owner: darkwing→dewey$/);
|
||
refused(() => step(d, "move", mv(9, "in-review", { candidate: "x" }), "darkwing", { candidate: CAND }), /only the claimant \(dewey\)/);
|
||
const unclaimed = step(genesisDoc(), "assign", { id: 11, seat: "rocko" }, "sage");
|
||
assert.equal(row(unclaimed, 11).claim, null);
|
||
});
|
||
|
||
test("render is byte-stable and escapes pipes", () => {
|
||
const d = step(genesisDoc(), "set", { id: 11, field: "piece", value: "a | b", reason: null }, "sage");
|
||
const a = render(d.rows, d.revision);
|
||
assert.equal(render(JSON.parse(JSON.stringify(d.rows)).reverse(), d.revision), a);
|
||
assert.match(a, /\| 11 \| a \\\| b \|/);
|
||
assert.match(a, /revision 1 by/);
|
||
assert.notEqual(sha256(render(d.rows, 2)), sha256(a));
|
||
});
|
||
|
||
test("view classification: current, genuine stale, edited stale marker, changed current body, markers", () => {
|
||
const d0 = genesisDoc();
|
||
const d1 = step(d0, "note", { id: 9, text: "one" }, "darkwing");
|
||
const wrap = (body) => `# Q\n\n<!-- mosaic-queue:begin -->\n${body}<!-- mosaic-queue:end -->\ntail\n`;
|
||
const cur = wrap(render(d1.rows, 1));
|
||
assert.equal(classifyView(cur, d1.log).state, "current");
|
||
const stale = classifyView(wrap(render(d0.rows, 0)), d1.log);
|
||
assert.equal(stale.state, "stale");
|
||
assert.deepEqual(stale.unshown.map((e) => e.rev), [1]);
|
||
assert.equal(classifyView(wrap(render(d0.rows, 0).replace("Row six", "Row 6")), d1.log).state, "unknown");
|
||
assert.equal(classifyView(cur.replace("| one |", "| two |"), d1.log).state, "unknown");
|
||
assert.equal(classifyView(cur.replace("<!-- mosaic-queue:end -->\n", ""), d1.log).state, "unknown");
|
||
assert.equal(classifyView(`${cur}<!-- mosaic-queue:begin -->\n`, d1.log).state, "unknown");
|
||
assert.equal(splitView("<!-- mosaic-queue:end -->\n<!-- mosaic-queue:begin -->\n"), null);
|
||
assert.equal(classifyView(null, d1.log).state, "unknown");
|
||
});
|
||
|
||
test("next: resume, then review, then start, then wait, then nothing; lowest id first", () => {
|
||
let d = genesisDoc();
|
||
const add = (owner, extra = {}) => ({ piece: `p-${owner}`, gate: "g", brief: "docs/plans/brief-b.md#Queue", owner, ...extra });
|
||
d = step(d, "add", add("rocko"), "sage", { brief: brief() }); // 12
|
||
d = step(d, "add", add("rocko", { reviewers: ["darkwing"], issues: [1508] }), "sage", { brief: brief() }); // 13
|
||
d = step(d, "add", add("darkwing"), "sage", { brief: brief() }); // 14
|
||
for (const id of [12, 13, 14]) d = step(d, "move", mv(id, "briefed"), "sage");
|
||
const rows = () => loadDoc(Buffer.from(serialize(d))).state.rows;
|
||
const all = () => true;
|
||
// darkwing resumes row 6 (claimed at genesis).
|
||
assert.deepEqual([nextFor(rows(), "darkwing", all).action, nextFor(rows(), "darkwing", all).row.id], ["resume", 6]);
|
||
d = step(d, "move", mv(13, "in-progress"), "rocko");
|
||
d = step(d, "move", mv(13, "in-review", { candidate: "x" }), "rocko", { candidate: CAND });
|
||
assert.equal(nextFor(rows(), "darkwing", all).row.id, 6);
|
||
d = step(d, "move", mv(6, "blocked", { reason: "r" }), "darkwing");
|
||
const r = nextFor(rows(), "darkwing", all);
|
||
assert.deepEqual([r.action, r.row.id], ["review", 13]);
|
||
// rocko: row 12 briefed, row 13 in review as author: start before wait.
|
||
assert.deepEqual([nextFor(rows(), "rocko", all).action, nextFor(rows(), "rocko", all).row.id], ["start", 12]);
|
||
const flagged = nextFor(rows(), "rocko", () => false);
|
||
assert.equal(flagged.briefDiffers, true);
|
||
d = step(d, "move", mv(12, "parked"), "jason");
|
||
assert.deepEqual([nextFor(rows(), "rocko", all).action, nextFor(rows(), "rocko", all).row.id], ["wait", 13]);
|
||
assert.equal(nextFor(rows(), "researcher", all).action, "nothing");
|
||
// row 9 (darkwing) waits on 6 settled, which blocked satisfies: start comes after review.
|
||
d = step(d, "move", mv(13, "in-progress"), "rocko");
|
||
assert.deepEqual([nextFor(rows(), "darkwing", all).action, nextFor(rows(), "darkwing", all).row.id], ["start", 9]);
|
||
});
|
||
|
||
test("canonical args make a retry's identity independent of list order", () => {
|
||
const a = canonArgs("add", { piece: "p", gate: "g", brief: "a.md#X", issues: [3, 1], reviewers: ["rocko", "dewey"] });
|
||
assert.deepEqual([a.issues, a.reviewers, a.after, a.required], [[1, 3], ["dewey", "rocko"], null, null]);
|
||
refused(() => canonArgs("add", { piece: "p", gate: "g", brief: "a.md#X", issues: [1, 1] }), /repeated/);
|
||
refused(() => canonArgs("add", { piece: "p", gate: "g", brief: "a.md" }), /PATH#ANCHOR/);
|
||
refused(() => canonArgs("add", { piece: "p", gate: "g", brief: "../x.md#A" }), /repo-relative/);
|
||
refused(() => canonArgs("move", { id: 1, to: "finished" }), /unknown state/);
|
||
refused(() => canonArgs("set", { id: 1, field: "owner", value: "x" }), /set cannot change/);
|
||
});
|
||
|
||
test("manifests, headings and blob ids", () => {
|
||
assert.equal(parseManifest(MANIFEST), 1);
|
||
refused(() => parseManifest(`${"c".repeat(64)} one-space\n`), /not "<sha256> <path>"/);
|
||
refused(() => parseManifest(`${"c".repeat(64)} a\n${"d".repeat(64)} a\n`), /twice/);
|
||
refused(() => parseManifest("no newline"), /end with a newline/);
|
||
const text = "# T\n\n## A\n\n```\n## A\n```\n\n### A \n";
|
||
assert.equal(countHeading(text, "A"), 2);
|
||
assert.equal(countHeading("~~~\n## A\n~~~\n## A\n", "A"), 1);
|
||
// git hash-object of "hello\n"
|
||
assert.equal(gitBlobId(Buffer.from("hello\n")), "ce013625030ba8dba906f756967f9e9ca394464a");
|
||
});
|
||
|
||
test("the migration map: one queue-map block, exact keys", () => {
|
||
assert.equal(parseMigrationMap(mapText()).rows.length, MAP_ROWS.length);
|
||
refused(() => parseMigrationMap(mapText() + mapText()), /exactly one/);
|
||
refused(() => parseMigrationMap("no block\n"), /exactly one/);
|
||
const bad = MAP_ROWS.map((r) => ({ ...r }));
|
||
delete bad[0].note;
|
||
refused(() => parseMigrationMap(mapText(bad)), /keys must be exactly/);
|
||
const noBrief = MAP_ROWS.map((r) => (r.id === 9 ? { ...r, brief: null } : r));
|
||
const map = parseMigrationMap(mapText(noBrief));
|
||
refused(() => loadDoc(Buffer.from(serialize(genesisDoc(map.rows)))), /only a done row may lack one/);
|
||
});
|