Files
stack/packages/queue/tests/store.test.mjs
T
jason.woltjeandClaude Opus 5.5 34a72af912 feat(queue): queue as data A1, journal, lock, CLI and verify (#1508)
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]>
2026-09-26 19:07:48 -05:00

352 lines
23 KiB
JavaScript

// The CLI against scratch repositories: genesis, the canonical checks (8.3),
// op ids (8.6), claims, the view (8.5 outcomes), brief checks (8.13),
// verify, render and snapshot.
import assert from "node:assert/strict";
import { mkdirSync, readFileSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { test } from "node:test";
import { LEGACY, MAP_ROWS, PKG_SRC, cli, genesisCommitted, mapText, queueMd, scratchRepo, sh } from "./helpers.mjs";
const MAP = "agents/sage/work/queue-migration-map.md";
function ok(r, re) {
assert.equal(r.code, 0, `exit ${r.code}: ${r.err}`);
if (re) assert.match(r.out, re);
return r;
}
function no(r, code, re) {
assert.equal(r.code, code, `expected exit ${code}, got ${r.code}: ${r.out}${r.err}`);
if (re) assert.match(r.err, re);
return r;
}
const genesisArgs = (repo, over = {}) => [
"genesis", "--op", over.op ?? "genesis-2026-09-26", "--root", over.root ?? repo.root, "--branch", over.branch ?? "refactor", "--map", over.map ?? MAP,
];
function ready(t, o = {}) {
const repo = scratchRepo(t, o);
genesisCommitted(repo);
return repo;
}
const doc = (repo) => JSON.parse(readFileSync(repo.queuePath, "utf8"));
const row = (repo, id) => doc(repo).rows.find((r) => r.id === id);
test("genesis: refusals before anything is written", (t) => {
const repo = scratchRepo(t);
no(cli(repo, ["list"]), 2, /no docs\/plans\/queue.json; genesis has not run/);
no(cli(repo, genesisArgs(repo), { by: "darkwing" }), 2, /only a privileged actor/);
no(cli(repo, genesisArgs(repo)), 2, /no actor: pass --by NAME or set MOSAIC_AGENT_NAME/);
no(cli(repo, genesisArgs(repo, { root: repo.base }), { by: "sage" }), 2, /is not this checkout's toplevel/);
no(cli(repo, genesisArgs(repo, { branch: "main" }), { by: "sage" }), 2, /HEAD is refs\/heads\/refactor; the queue runs only on branch main/);
no(cli(repo, genesisArgs(repo, { map: "agents/sage/work/nope.md" }), { by: "sage" }), 2, /not a committed file in HEAD/);
no(cli(repo, genesisArgs(repo, { op: "short" }), { by: "sage" }), 2, /must match/);
const missingOp = genesisArgs(repo).filter((a, i, all) => a !== "--op" && all[i - 1] !== "--op");
no(cli(repo, missingOp, { by: "sage" }), 4, /--op ID is required/);
assert.throws(() => readFileSync(repo.queuePath));
});
test("genesis: the map must be committed, well formed, with committed briefs and seats", (t) => {
const uncommitted = scratchRepo(t, { commitMap: false });
no(cli(uncommitted, genesisArgs(uncommitted), { by: "sage" }), 2, /not a committed file in HEAD/);
const twice = scratchRepo(t, { map: mapText() + mapText() });
no(cli(twice, genesisArgs(twice), { by: "sage" }), 2, /exactly one ```json queue-map block/);
const anchor = scratchRepo(t, { map: mapText(MAP_ROWS.map((r) => (r.id === 11 ? { ...r, brief: { path: "docs/plans/brief-b.md", anchor: "Nope" } } : r))) });
no(cli(anchor, genesisArgs(anchor), { by: "sage" }), 2, /has 0 headings "Nope" in HEAD/);
const staged = scratchRepo(t, { map: mapText(MAP_ROWS.map((r) => (r.id === 11 ? { ...r, brief: { path: "docs/plans/new.md", anchor: "New" } } : r))) });
writeFileSync(join(staged.root, "docs/plans/new.md"), "## New\n");
staged.g("add", "docs/plans/new.md");
no(cli(staged, genesisArgs(staged), { by: "sage" }), 2, /new.md is not committed in HEAD; commit it first \(a staged brief is refused\)/);
const ghost = scratchRepo(t, { map: mapText(MAP_ROWS.map((r) => (r.id === 11 ? { ...r, owner: "ghost" } : r))) });
no(cli(ghost, genesisArgs(ghost), { by: "sage" }), 2, /row 11 owner ghost is not jason, coordinator, unassigned or a seat/);
const noBrief = scratchRepo(t, { map: mapText(MAP_ROWS.map((r) => (r.id === 11 ? { ...r, brief: null } : r))) });
no(cli(noBrief, genesisArgs(noBrief), { by: "sage" }), 2, /only a done row may lack one/);
});
test("genesis: markers, a stray witness, once only; a retry returns the receipt", (t) => {
const repo = scratchRepo(t);
writeFileSync(repo.viewPath, "# QUEUE\n\nno markers\n");
no(cli(repo, genesisArgs(repo), { by: "sage" }), 2, /needs the two queue markers/);
writeFileSync(repo.viewPath, queueMd());
writeFileSync(join(repo.gitDir, "mosaic-queue.head"), "{}\n");
no(cli(repo, genesisArgs(repo), { by: "sage" }), 2, /the witness .* exists without docs\/plans\/queue.json/);
unlinkSync(join(repo.gitDir, "mosaic-queue.head"));
const g = ok(cli(repo, genesisArgs(repo), { by: "sage" }), /^ok genesis-2026-09-26 rev 0 genesis 5 rows$/m);
const d = doc(repo);
assert.equal(d.canonicalRoot, repo.root);
assert.equal(d.log[0].result.legacyView, LEGACY);
assert.equal(d.log[0].result.mapBlob, repo.g("rev-parse", `HEAD:${MAP}`).trim());
assert.match(readFileSync(repo.viewPath, "utf8"), /^# QUEUE\n\nHeader prose\.\n\n<!-- mosaic-queue:begin -->\n\nGenerated from `docs\/plans\/queue.json` revision 0/);
assert.match(readFileSync(repo.viewPath, "utf8"), /<!-- mosaic-queue:end -->\n\nParked entries below\.\n$/);
ok(cli(repo, genesisArgs(repo), { by: "sage" }), new RegExp(`^${g.out.trim()} \\(already recorded at rev 0\\)`));
no(cli(repo, genesisArgs(repo, { op: "genesis-other-01" }), { by: "sage" }), 2, /exists; genesis runs once/);
// Before the genesis commit, reads work and ops refuse.
ok(cli(repo, ["list"]), /^1\tdone\tdarkwing\tFinished thing\n6\tin-progress/);
no(cli(repo, ["note", "9", "x", "--op", "note-row9-001"], { by: "darkwing" }), 2, /genesis not committed: HEAD has no docs\/plans\/queue.json/);
repo.g("add", "docs/plans/queue.json", "docs/plans/QUEUE.md");
repo.g("commit", "-q", "-m", "genesis");
rmSync(repo.queuePath);
no(cli(repo, genesisArgs(repo, { op: "genesis-again-1" }), { by: "sage" }), 2, /HEAD already has docs\/plans\/queue.json|witness .* exists/);
});
test("genesis: a file holding genesis alone with no witness is confirmed by sync or a retry", (t) => {
const repo = ready(t);
unlinkSync(join(repo.gitDir, "mosaic-queue.head"));
no(cli(repo, ["list"]), 2, /witness .* is missing; .*run `scripts\/mosaic queue sync` \(the file holds genesis alone\)/);
ok(cli(repo, genesisArgs(repo), { by: "sage" }), /already recorded at rev 0/);
unlinkSync(join(repo.gitDir, "mosaic-queue.head"));
ok(cli(repo, ["sync"]), /durable now, never acknowledged: genesis-2026-09-26 by sage/);
ok(cli(repo, ["sync"]), /nothing to confirm: rev 0 is durable and witnessed/);
});
test("canonical checks: worktree, second clone, detached HEAD, wrong branch, GIT_DIR, foreign code; a symlink works", (t) => {
const repo = ready(t);
ok(cli(repo, ["verify"]), /^ok verify rev 0/);
const wt = join(repo.base, "wt");
repo.g("worktree", "add", "-q", "--detach", wt);
no(cli(repo, ["list"], { cwd: wt }), 2, /is a linked worktree; the queue runs only in the canonical checkout/);
const clone = join(repo.base, "clone");
sh("git", ["clone", "-q", "--branch", "refactor", repo.root, clone], { env: repo.env });
no(cli(repo, ["list"], { cwd: clone }), 2, new RegExp(`this checkout is ${clone}; the queue's canonical root is ${repo.root}`));
const cloneCli = { ...repo, root: clone };
no(cli(cloneCli, ["list"], { cwd: clone }), 2, /the queue's canonical root is/);
repo.g("checkout", "-q", "--detach");
no(cli(repo, ["list"]), 2, /HEAD is detached; the queue runs only on branch refactor/);
repo.g("checkout", "-q", "-b", "other");
no(cli(repo, ["list"]), 2, /HEAD is refs\/heads\/other; the queue runs only on branch refactor/);
repo.g("checkout", "-q", "refactor");
no(cli(repo, ["list"], { env: { GIT_DIR: repo.gitDir } }), 2, /GIT_DIR is set/);
no(cli(repo, ["list"], { env: { GIT_WORK_TREE: repo.root } }), 2, /GIT_WORK_TREE is set/);
no(cli(repo, ["list"], { cwd: repo.base }), 2, /is not inside a git checkout/);
const foreign = sh(process.execPath, [join(PKG_SRC, "cli.mjs"), "list"], { cwd: repo.root, env: repo.env, allowFail: true });
assert.equal(foreign.status, 2);
assert.match(foreign.stderr, /this queue code .* is not under the canonical root/);
const link = join(repo.base, "link");
symlinkSync(repo.root, link);
ok(cli(repo, ["list"], { cwd: join(link, "docs") }), /^1\tdone/);
});
test("op ids: missing, too long, reserved; a retry answers; another payload refuses", (t) => {
const repo = ready(t);
no(cli(repo, ["note", "9", "hello"], { by: "darkwing" }), 4, /--op ID is required/);
no(cli(repo, ["note", "9", "hello", "--op", "note-by-ghost"], { by: "ghost" }), 2, /actor ghost is not jason or a seat under agents/);
no(cli(repo, ["note", "9", "hello", "--op", "note-by-nobody"]), 2, /no actor/);
no(cli(repo, ["note", "9", "hello", "--op", "a".repeat(73)], { by: "darkwing" }), 2, /8 to 72 characters/);
ok(cli(repo, ["note", "9", "hello", "--op", "a".repeat(72)], { by: "darkwing" }));
no(cli(repo, ["note", "9", "hello", "--op", "note-row9.outcome"], { by: "darkwing" }), 2, /reserved/);
const first = ok(cli(repo, ["note", "9", "hi", "--op", "note-row9-0001"], { by: "darkwing" }), /^ok note-row9-0001 rev 2 row 9 note$/m);
ok(cli(repo, ["note", "9", "hi", "--op", "note-row9-0001"], { by: "darkwing" }), new RegExp(`^${first.out.trim()} \\(already recorded at rev 2\\)$`, "m"));
no(cli(repo, ["note", "9", "other", "--op", "note-row9-0001"], { by: "darkwing" }), 2, /recorded at rev 2 as note with other arguments; a new operation needs a new op id/);
no(cli(repo, ["release", "9", "--op", "note-row9-0001"], { by: "darkwing" }), 2, /as note with other arguments/);
no(cli(repo, ["move", "6", "in-progress", "--op", "move-row6-again"], { by: "darkwing" }), 2, /not a transition/);
assert.equal(doc(repo).revision, 2);
});
test("a retried add returns the id it first allocated, after reassignment and after done", (t) => {
const repo = ready(t);
const add = ["add", "--op", "add-new-row-01", "--piece", "New thing", "--gate", "Jason says", "--brief", "docs/plans/brief-b.md#Template", "--owner", "rocko", "--issue", "#1508"];
const first = ok(cli(repo, add, { by: "sage" }), /row 12 none→queued$/m).out.trim();
ok(cli(repo, ["assign", "12", "dewey", "--op", "assign-12-dewey"], { by: "sage" }), /owner: rocko→dewey/);
ok(cli(repo, add, { by: "sage" }), new RegExp(`^${first} \\(already recorded at rev 1\\)$`, "m"));
ok(cli(repo, ["move", "12", "briefed", "--op", "brief-12-0001"], { by: "sage" }));
ok(cli(repo, ["move", "12", "in-progress", "--op", "start-12-0001"], { by: "dewey" }));
ok(cli(repo, ["move", "12", "in-review", "--candidate", "HEAD", "--op", "review-12-0001"], { by: "dewey" }), /in-progress→in-review round 1/);
ok(cli(repo, ["move", "12", "waiting-on-jason", "--op", "wait-12-00001"], { by: "sage" }));
ok(cli(repo, ["move", "12", "done", "--op", "done-12-00001"], { by: "jason" }));
ok(cli(repo, add, { by: "sage" }), new RegExp(`^${first} \\(already recorded at rev 1\\)$`, "m"));
assert.equal(doc(repo).rows.filter((r) => r.piece === "New thing").length, 1);
});
test("Rocko's S4 schedule: a lost result, another writer, then the retry opens no second round", (t) => {
const repo = ready(t);
ok(cli(repo, ["move", "6", "blocked", "--reason", "paused", "--op", "block-6-00001"], { by: "darkwing" }));
ok(cli(repo, ["move", "9", "in-progress", "--op", "start-9-00001"], { by: "darkwing" }));
const review = ["move", "9", "in-review", "--candidate", "HEAD", "--op", "review-9-00001"];
cli(repo, review, { by: "darkwing" }); // result lost
ok(cli(repo, ["note", "9", "looking now", "--op", "note-9-000001"], { by: "filbert" }));
ok(cli(repo, review, { by: "darkwing" }), /round 1 on #1508 \(already recorded at rev 3\)/);
assert.equal(row(repo, 9).review.rounds.length, 1);
});
test("the review issue and the evidence round through the CLI (lead decision 23, 8.7)", (t) => {
const repo = ready(t);
ok(cli(repo, ["move", "6", "blocked", "--reason", "paused", "--op", "block-6-00001"], { by: "darkwing" }));
ok(cli(repo, ["set", "9", "issues", "1495,1508", "--op", "issues-9-0001"], { by: "sage" }));
ok(cli(repo, ["move", "9", "in-progress", "--op", "start-9-00001"], { by: "darkwing" }));
const review = (op, ...extra) => ["move", "9", "in-review", "--candidate", "HEAD", "--op", op, ...extra];
no(cli(repo, review("review-9-00001"), { by: "darkwing" }), 2, /lists several issues \(#1495, #1508\); name the review's issue with --issue/);
no(cli(repo, review("review-9-00001", "--issue", "1495", "--issue", "1508"), { by: "darkwing" }), 4, /one --issue/);
no(cli(repo, review("review-9-00001", "--issue", "#1600"), { by: "darkwing" }), 2, /--issue #1600 is not one of row 9's issues/);
ok(cli(repo, review("review-9-00001", "--issue", "#1508"), { by: "darkwing" }), /in-progress→in-review round 1 on #1508$/m);
const head = repo.g("rev-parse", "HEAD").trim();
no(cli(repo, ["move", "9", "done", "--evidence", `comment=7,candidate=${head}`, "--op", "done-9-000001"], { by: "filbert" }), 2, /round=<n>/);
ok(cli(repo, ["move", "9", "in-progress", "--op", "changes-9-0001"], { by: "darkwing" }));
ok(cli(repo, review("review-9-00002"), { by: "darkwing" }), /round 2 on #1508$/m);
no(cli(repo, ["move", "9", "done", "--evidence", `comment=7,round=1,candidate=${head}`, "--op", "done-9-000001"], { by: "filbert" }), 2, /evidence names round 1; row 9 is in round 2/);
ok(cli(repo, ["move", "9", "done", "--evidence", `comment=8,round=2,candidate=${head}`, "--op", "done-9-000002"], { by: "filbert" }), /in-review→done round 2$/m);
});
test("claims and add defaults through the CLI; candidates are manifests or reachable commits", (t) => {
const repo = ready(t);
ok(cli(repo, ["add", "--op", "add-by-dewey-1", "--piece", "Mine", "--gate", "tests", "--brief", "docs/plans/brief-b.md#Template"], { by: "dewey" }));
const r = row(repo, 12);
assert.deepEqual([r.owner, r.gateOwner, r.state, r.claim, r.brief.blob], ["dewey", "jason", "queued", null, repo.g("rev-parse", "HEAD:docs/plans/brief-b.md").trim()]);
no(cli(repo, ["add", "--op", "add-by-dewey-2", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Template", "--reviewer", "ghost"], { by: "sage" }), 2, /reviewer ghost is not jason or a seat/);
no(cli(repo, ["add", "--op", "add-by-dewey-3", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md"], { by: "dewey" }), 2, /PATH#ANCHOR/);
no(cli(repo, ["add", "--op", "add-by-dewey-4", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Queue", "--issue", "x"], { by: "dewey" }), 4);
ok(cli(repo, ["move", "11", "in-progress", "--op", "start-11-0001"], { by: "dewey" }));
assert.deepEqual(row(repo, 11).claim, { seat: "dewey", op: "start-11-0001" });
no(cli(repo, ["move", "11", "in-review", "--candidate", "no-such-thing", "--op", "review-11-001"], { by: "dewey" }), 2, /neither a manifest file nor a commit/);
const dangling = repo.g("commit-tree", "-m", "x", `${repo.g("rev-parse", "HEAD^{tree}").trim()}`).trim();
no(cli(repo, ["move", "11", "in-review", "--candidate", dangling, "--op", "review-11-002"], { by: "dewey" }), 2, /not reachable from any local branch or tag/);
const manifest = join(repo.base, "cand.sha256");
writeFileSync(manifest, `${"e".repeat(64)} packages/x.mjs\n`);
ok(cli(repo, ["move", "11", "in-review", "--candidate", manifest, "--op", "review-11-003"], { by: "dewey" }));
assert.equal(row(repo, 11).review.rounds[0].candidate.kind, "manifest");
ok(cli(repo, ["release", "6", "--op", "release-6-001"], { by: "sage" }), /in-progress→briefed/);
assert.equal(row(repo, 6).claim, null);
});
test("the working-brief check: a changed working copy refuses the start and flags next", (t) => {
const repo = ready(t);
writeFileSync(join(repo.root, "docs/plans/brief-b.md"), "# Briefs B\n\n## Queue\n\nEdited.\n\n## Template\n\nEdited.\n");
ok(cli(repo, ["next", "dewey"]), /^start row 11: Brief template; brief docs\/plans\/brief-b.md § Template; brief differs from pinned blob; ask the lead to re-pin$/m);
no(cli(repo, ["move", "11", "in-progress", "--op", "start-11-0001"], { by: "dewey" }), 2, /row 11: brief differs from pinned blob; ask the lead to re-pin/);
repo.g("commit", "-q", "-am", "edit brief");
no(cli(repo, ["move", "11", "in-progress", "--op", "start-11-0001"], { by: "dewey" }), 2, /brief differs from pinned blob/);
no(cli(repo, ["verify", "--current"]), 2, /brief drift against HEAD:\nrow 9: brief docs\/plans\/brief-b.md changed in HEAD .*\nrow 11:/);
ok(cli(repo, ["verify"]));
ok(cli(repo, ["set", "11", "brief", "docs/plans/brief-b.md#Template", "--op", "repin-11-0001"], { by: "sage" }), /row 11 brief: docs\/plans\/brief-b.md § Template @\w{12}→docs\/plans\/brief-b.md § Template @\w{12}/);
ok(cli(repo, ["next", "dewey"]), /^start row 11: Brief template; brief docs\/plans\/brief-b.md § Template$/m);
ok(cli(repo, ["move", "11", "in-progress", "--op", "start-11-0001"], { by: "dewey" }));
});
test("next: resume first, then nothing for an idle seat; needs a seat", (t) => {
const repo = ready(t);
ok(cli(repo, ["next", "darkwing"]), /^resume row 6: Row six/m);
ok(cli(repo, ["next"], { by: "dewey" }), /^start row 11/m);
ok(cli(repo, ["next", "rocko"]), /^nothing$/m);
no(cli(repo, ["next"]), 2, /next needs a seat/);
});
test("view stale: new ops and verify refuse naming the unshown op; retries answer; reads warn; render fixes", (t) => {
const repo = ready(t);
const first = ok(cli(repo, ["note", "9", "one", "--op", "note-9-000001"], { by: "darkwing" })).out.trim();
repo.g("checkout", "--", "docs/plans/QUEUE.md");
no(cli(repo, ["note", "9", "two", "--op", "note-9-000002"], { by: "darkwing" }), 2, /view stale: QUEUE.md shows rev 0; rev 1 \(op note-9-000001 by darkwing at .*\) is recorded .*Tell darkwing, then run `scripts\/mosaic queue render`/);
no(cli(repo, ["verify"]), 2, /view stale/);
const retry = ok(cli(repo, ["note", "9", "one", "--op", "note-9-000001"], { by: "darkwing" }));
assert.equal(retry.out.trim(), `${first} (already recorded at rev 1)`);
assert.match(retry.err, /warning: view stale/);
const read = ok(cli(repo, ["list"]));
assert.match(read.err, /warning: view stale/);
no(cli(repo, ["render", "--check"]), 2, /view stale/);
ok(cli(repo, ["render"]), /rendered rev 1 over rev 0; newly shown: rev 1 \(op note-9-000001 by darkwing/);
ok(cli(repo, ["render"]), /view current at rev 1; nothing written/);
ok(cli(repo, ["verify"]));
});
test("view unknown: a hand edit, an old marker over an edited body, missing or duplicate markers", (t) => {
const repo = ready(t);
ok(cli(repo, ["note", "9", "one", "--op", "note-9-000001"], { by: "darkwing" }));
const good = readFileSync(repo.viewPath, "utf8");
const cases = [
good.replace("| one |", "| two |"),
repo.g("show", "HEAD:docs/plans/QUEUE.md").replace("Row six", "Row 6"),
good.replace("<!-- mosaic-queue:end -->\n", ""),
good + "<!-- mosaic-queue:begin -->\n",
];
for (const text of cases) {
writeFileSync(repo.viewPath, text);
no(cli(repo, ["note", "9", "two", "--op", "note-9-000002"], { by: "darkwing" }), 2, /view unknown: .*restore the table with git or re-apply the edit as queue ops/);
no(cli(repo, ["render"]), 2, /view unknown/);
no(cli(repo, ["verify"]), 2, /view unknown/);
assert.match(ok(cli(repo, ["show", "9"])).err, /warning: view unknown/);
assert.equal(readFileSync(repo.viewPath, "utf8"), text, "nothing rewrote an unknown view");
}
writeFileSync(repo.viewPath, good.replace("Header prose.", "Header prose, edited by hand."));
ok(cli(repo, ["note", "9", "two", "--op", "note-9-000002"], { by: "darkwing" }));
assert.match(readFileSync(repo.viewPath, "utf8"), /edited by hand/);
});
test("a hand edit to queue.json refuses every verb, reads included", (t) => {
const repo = ready(t);
const text = readFileSync(repo.queuePath, "utf8");
writeFileSync(repo.queuePath, text.replace('"piece": "Row six"', '"piece": "Row 6"'));
for (const args of [["list"], ["show", "6"], ["next", "darkwing"], ["verify"], ["render"], ["sync"]]) no(cli(repo, args), 2, /do not equal the replay/);
no(cli(repo, ["note", "9", "x", "--op", "note-9-000001"], { by: "darkwing" }), 2, /do not equal the replay/);
writeFileSync(repo.queuePath, text.replace("\n", "\n\n"));
no(cli(repo, ["list"]), 2, /re-serialize byte for byte/);
writeFileSync(repo.queuePath, "{ not json");
no(cli(repo, ["list"]), 2, /not valid JSON|is not JSON/);
});
test("verify and render --check leave bytes and mtimes unchanged", (t) => {
const repo = ready(t);
const snap = () => [repo.queuePath, repo.viewPath, join(repo.gitDir, "mosaic-queue.head")].map((p) => [readFileSync(p, "hex"), statSync(p, { bigint: true }).mtimeNs]);
const before = snap();
ok(cli(repo, ["verify"]));
ok(cli(repo, ["verify", "--current"]));
ok(cli(repo, ["render", "--check"]));
ok(cli(repo, ["render"]));
ok(cli(repo, ["list"]));
ok(cli(repo, ["next", "darkwing"]));
assert.deepEqual(snap(), before);
});
test("render is byte-stable across runs and repositories", (t) => {
const a = ready(t);
const b = ready(t);
const body = (repo) => readFileSync(repo.viewPath, "utf8").replace(repo.root, "ROOT");
assert.equal(body(a), body(b));
const bytes = readFileSync(a.viewPath);
writeFileSync(join(a.base, "x"), "");
ok(cli(a, ["render"]));
assert.ok(readFileSync(a.viewPath).equals(bytes));
});
test("snapshot and verify --snapshot", (t) => {
const repo = ready(t);
const out = join(repo.base, "snap");
mkdirSync(out);
no(cli(repo, ["snapshot", "--out", join(repo.root, "docs")]), 2, /is not empty/);
mkdirSync(join(repo.root, "empty-inside"));
no(cli(repo, ["snapshot", "--out", "empty-inside"]), 2, /is inside the repository/);
no(cli(repo, ["snapshot", "--out", join(repo.base, "missing")]), 2, /does not exist/);
ok(cli(repo, ["snapshot", "--out", out]), /^snapshot rev 0: queue.json [0-9a-f]{64}, QUEUE.md [0-9a-f]{64}$/m);
ok(cli(repo, ["verify", "--snapshot", out, "--base-absent"], { cwd: repo.base }), /genesis alone/);
no(cli(repo, ["verify", "--snapshot", out]), 4, /exactly one of/);
const base = join(repo.base, "base.json");
writeFileSync(base, readFileSync(repo.queuePath));
ok(cli(repo, ["note", "9", "one", "--op", "note-9-000001"], { by: "darkwing" }));
const out2 = join(repo.base, "snap2");
mkdirSync(out2);
ok(cli(repo, ["snapshot", "--out", out2]));
ok(cli(repo, ["verify", "--snapshot", out2, "--base-file", base], { cwd: repo.base }), /extends the base/);
no(cli(repo, ["verify", "--snapshot", out2, "--base-absent"]), 2, /must hold genesis alone/);
no(cli(repo, ["verify", "--snapshot", out, "--base-file", join(out2, "queue.json")]), 2, /does not extend the base/);
writeFileSync(join(out2, "QUEUE.md"), readFileSync(join(out, "QUEUE.md")));
no(cli(repo, ["verify", "--snapshot", out2, "--base-file", base]), 2, /is not the render of rev 1 \(stale\)/);
repo.g("checkout", "--", "docs/plans/QUEUE.md");
const out3 = join(repo.base, "snap3");
mkdirSync(out3);
no(cli(repo, ["snapshot", "--out", out3]), 2, /view stale/);
});
test("usage errors exit 4", (t) => {
const repo = ready(t);
no(cli(repo, []), 4);
no(cli(repo, ["frobnicate"]), 4, /unknown verb/);
no(cli(repo, ["list", "--bogus"]), 4, /unknown option --bogus/);
no(cli(repo, ["show"]), 4, /expected show ID/);
no(cli(repo, ["show", "0"]), 4, /positive integer/);
no(cli(repo, ["move", "9", "done", "--op", "x-00000001", "--op", "y-00000001"], { by: "sage" }), 4, /--op given twice/);
no(cli(repo, ["set", "9", "owner", "x", "--op", "set-9-000001"], { by: "sage" }), 4, /set fields/);
no(cli(repo, ["set", "9", "required", "yes", "--op", "set-9-000001"], { by: "sage" }), 4, /true or false/);
no(cli(repo, ["add", "--op", "add-0000001", "--piece", "x"], { by: "sage" }), 4, /add needs --gate/);
no(cli(repo, ["list", "--check"]), 4, /does not apply here/);
ok(cli(repo, ["--help"]), /^usage: queue list/);
});