packages/queue, scripts/queue-commit.sh, scripts/git-hooks and scripts/test-queue.sh, plus docs/plans/BRIEF-TEMPLATE.md. There is no queue.json yet, so verify skips until the genesis commit after A2. Darkwing built it, and Filbert reviewed R0 (6933b885, changes requested) and r1 (e464be6c, approved). The 20 files match manifest 85a8a453. The nine suites passed on an index export, including the new queue suite. test-queue.sh joins the suite list in AGENTS.md. Lead decisions 20, 23 and 26. Co-Authored-By: Claude Opus 5.5 <[email protected]>
513 lines
31 KiB
JavaScript
513 lines
31 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) {
|
||
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, "genesis-op-0001", t, "sage");
|
||
const body = render(grows, 0);
|
||
const entry = {
|
||
rev: 0, op: "genesis-op-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-op-0001", 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.equal(rv.issue, 1508);
|
||
assert.deepEqual(rv.rounds.map((r) => [r.n, r.request, r.candidate.digest]), [[1, "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");
|
||
|
||
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(row(one, 12).review.issue, 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(row(d, 12).review.issue, 1508);
|
||
// Later rounds keep the previous round's issue unless --issue names another.
|
||
d = review(again(d));
|
||
assert.deepEqual([row(d, 12).review.issue, row(d, 12).review.rounds.length], [1508, 2]);
|
||
d = review(again(d), { issue: 1495 });
|
||
assert.deepEqual([row(d, 12).review.issue, row(d, 12).review.rounds.length], [1495, 3]);
|
||
// 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(row(review(d, { issue: 1600 }), 12).review.issue, 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 review with a null issue", () => {
|
||
const r = structuredClone(row(review(row12Started([1508])), 12));
|
||
validateRow(r);
|
||
r.review.issue = null;
|
||
refused(() => validateRow(r), /review issue must be a positive integer/);
|
||
});
|
||
|
||
// 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, [1508, 1510]);
|
||
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("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/);
|
||
});
|