Row 101. Built by a Haiku subagent under Researcher (decision 129),
packet agents/researcher/work/queue-101/r1/, candidate manifest
ee1335a6e44610f676ade116733d180ac6f0b3603a58306110cbd1dd1e9c1f69.
Filbert approved on #1595, comment 28245.
The check runs at write time in store.mjs mutateAs, with no SEMANTICS
bump, so the live Console keeps reading the log.
Sage's gate rerun on 79e126be0 with rows 101 and 105 applied, Docker
up: node suites bus 128, business 67, cli 146, control-board 124,
conversation 182, discord 178, harness 70, ledger 78, mosaic 69,
queue 155, runs 43, seat 109, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
401 lines
27 KiB
JavaScript
401 lines
27 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/);
|
|
// The owner-not-reviewer rule of add, set and assign (2026-10-04).
|
|
const selfReview = scratchRepo(t, { map: mapText(MAP_ROWS.map((r) => (r.id === 9 ? { ...r, reviewers: ["darkwing", "filbert"] } : r))) });
|
|
no(cli(selfReview, genesisArgs(selfReview), { by: "sage" }), 2, /migration map row 9 owner darkwing can't be a listed reviewer/);
|
|
for (const repo of [ghost, noBrief, selfReview]) {
|
|
assert.throws(() => readFileSync(repo.queuePath));
|
|
assert.throws(() => readFileSync(join(repo.gitDir, "mosaic-queue.head")));
|
|
assert.equal(readFileSync(repo.viewPath, "utf8"), queueMd());
|
|
}
|
|
});
|
|
|
|
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", "--gate-owner", "filbert", "--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);
|
|
// No reviewers, so the round posts no request; review.test.mjs covers that path.
|
|
ok(cli(repo, ["set", "9", "reviewers", "none", "--op", "reviewers-9-none"], { by: "sage" }));
|
|
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: "sage" }));
|
|
ok(cli(repo, review, { by: "darkwing" }), /round 1 on #1508 \(already recorded at rev 4\)/);
|
|
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, ["set", "9", "reviewers", "none", "--op", "reviewers-9-none"], { by: "sage" }));
|
|
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", "--gate-owner", "filbert", "--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("add, set reviewers and assign refuse the row's owner as a reviewer", (t) => {
|
|
const repo = ready(t);
|
|
const before = doc(repo);
|
|
no(cli(repo, ["add", "--op", "add-owner-reviewer", "--piece", "x", "--gate", "g", "--brief", "docs/plans/brief-b.md#Template", "--owner", "rocko", "--gate-owner", "filbert", "--reviewer", "rocko"], { by: "sage" }), 2, /row 12 owner rocko can't be a listed reviewer/);
|
|
assert.deepEqual(doc(repo), before);
|
|
no(cli(repo, ["set", "9", "reviewers", "filbert,darkwing", "--op", "set-9-owner-reviewer"], { by: "sage" }), 2, /row 9 owner darkwing can't be a listed reviewer/);
|
|
assert.deepEqual(doc(repo), before);
|
|
no(cli(repo, ["assign", "9", "filbert", "--op", "assign-9-filbert"], { by: "sage" }), 2, /can't assign row 9 to filbert: filbert is one of its reviewers/);
|
|
assert.deepEqual(doc(repo), before);
|
|
// The unchanged accept paths: a reviewer list or a new owner that does not collide with the other.
|
|
ok(cli(repo, ["set", "9", "reviewers", "filbert,dewey", "--op", "set-9-ok"], { by: "sage" }));
|
|
assert.deepEqual(row(repo, 9).reviewers, ["dewey", "filbert"]);
|
|
ok(cli(repo, ["assign", "11", "rocko", "--op", "assign-11-rocko"], { by: "sage" }));
|
|
assert.equal(row(repo, 11).owner, "rocko");
|
|
});
|
|
|
|
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", "--gate-owner", "filbert"], { by: "sage" }), 4, /add needs --gate/);
|
|
no(cli(repo, ["list", "--check"]), 4, /does not apply here/);
|
|
ok(cli(repo, ["--help"]), /^usage: queue list/);
|
|
});
|