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]>
374 lines
25 KiB
JavaScript
374 lines
25 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("--by that differs from MOSAIC_AGENT_NAME warns on stderr and logs nothing more (N12)", (t) => {
|
|
const warn = (by, env) => `warning: --by ${JSON.stringify(by)} differs from MOSAIC_AGENT_NAME=${JSON.stringify(env)}`;
|
|
// Genesis goes through the same check.
|
|
const fresh = scratchRepo(t);
|
|
const g = ok(cli(fresh, [...genesisArgs(fresh), "--by", "sage"], { by: "darkwing" }), /^ok genesis-2026-09-26 rev 0 genesis/);
|
|
assert.equal(g.err, `${warn("sage", "darkwing")}\n`);
|
|
const repo = ready(t);
|
|
// --by wins and the log records only the actor.
|
|
const r = ok(cli(repo, ["note", "9", "hello", "--op", "note-n12-0001", "--by", "darkwing"], { by: "dewey" }), /^ok note-n12-0001 rev 1/);
|
|
assert.equal(r.err.split("\n")[0], warn("darkwing", "dewey"));
|
|
const e = doc(repo).log.at(-1);
|
|
assert.deepEqual([e.by, Object.keys(e).join()], ["darkwing", "rev,op,verb,args,by,at,semantics,result,viewSha"]);
|
|
// A refusal carries it too.
|
|
const n = no(cli(repo, ["note", "9", "again", "--op", "note-n12-0002", "--by", "rocko"], { by: "darkwing" }), 2, /only the owner/);
|
|
assert.equal(n.err.trimEnd().split("\n").at(-1), warn("rocko", "darkwing"));
|
|
// Same name, or only one of the two: no warning.
|
|
assert.equal(ok(cli(repo, ["note", "9", "three", "--op", "note-n12-0003", "--by", "darkwing"], { by: "darkwing" })).err, "");
|
|
assert.equal(ok(cli(repo, ["note", "9", "four", "--op", "note-n12-0004"], { by: "darkwing" })).err, "");
|
|
assert.equal(ok(cli(repo, ["note", "9", "five", "--op", "note-n12-0005", "--by", "darkwing"])).err, "");
|
|
assert.equal(ok(cli(repo, ["note", "9", "six", "--op", "note-n12-0006", "--by", "darkwing"], { by: "" })).err, "");
|
|
});
|
|
|
|
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/);
|
|
});
|