Files
stack/packages/queue/tests/review.test.mjs
T
jason.woltjeandClaude Opus 5.5 f539466fcb feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)
queue move ID in-review posts the review request as a Gitea comment and
review record reads verdicts back, so reviews stop being files in
docs/plans/reviews/. On a comment round, in-review to waiting-on-jason
now needs every listed reviewer's approval for the current round, the
same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads
the raw per-seat token files (lead decisions 37 to 39): config built and
checked before curl starts, export attribute cleared, fixed base URL.
test-queue.sh skips its live checks outside the canonical root.

Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302)
and corrected the plan (293747cd). Rocko reviewed the helper (e896192f,
2096b0a3), and Sage's lead check passed under decision 38. Manifest
b402fb38, 19 files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 10:07:29 -05:00

665 lines
48 KiB
JavaScript

// Piece D (8.9): the request comment, its outcome, resolve and abandon,
// recorded verdicts and verify-commit. Every test posts to
// fixtures/fake-gitea.mjs, installed as the scratch repository's
// scripts/gitea-api.sh. The token files are dummies; nothing reads them.
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { test } from "node:test";
import { pathToFileURL } from "node:url";
import { cli, genesisCommitted, load, opts, scratchRepo } from "./helpers.mjs";
const HERE = new URL(".", import.meta.url).pathname;
const FAKE = join(HERE, "fixtures", "fake-gitea.mjs");
const LOGINS = ["darkwing", "dewey", "filbert", "rocko", "jarvis", "sage"];
async function ready(t, { start = true } = {}) {
const repo = scratchRepo(t);
genesisCommitted(repo);
mkdirSync(join(repo.root, "scripts"), { recursive: true });
writeFileSync(join(repo.root, "scripts/gitea-api.sh"), `#!/bin/sh\nexec "${process.execPath}" "${FAKE}" "$@"\n`, { mode: 0o755 });
const logFile = join(repo.base, "gitea.log");
const scenarioFile = join(repo.base, "scenario.json");
repo.env = { ...repo.env, FAKE_GITEA_LOG: logFile, FAKE_GITEA_SCENARIO: scenarioFile };
const tokens = {};
for (const login of LOGINS) {
const dir = join(repo.base, "fleet/agents", login, "secrets");
mkdirSync(dir, { recursive: true });
tokens[login] = join(dir, `gitea-mosaicstack-${login}.token`);
writeFileSync(tokens[login], `${"0".repeat(40)}\n`, { mode: 0o600 });
}
const m = await load(repo);
const cred = (seat) => ({ MOSAIC_GITEA_CREDENTIAL_FILE: tokens[seat === "sage" ? "jarvis" : seat] });
const s = {
repo, m, tokens, cred,
run: (args, by, env = {}) => cli(repo, args, { by, env: { ...cred(by), ...env } }),
mut: (req, extra = {}) => m.store.mutate(opts(repo, { ...extra, env: { ...repo.env, ...cred(req.by), ...(extra.env ?? {}) } }), req),
calls: () => (existsSync(logFile) ? readFileSync(logFile, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)) : []),
posts: () => s.calls().filter((c) => c.method === "POST"),
rules: (rules) => writeFileSync(scenarioFile, JSON.stringify({ rules })),
doc: () => JSON.parse(readFileSync(repo.queuePath, "utf8")),
row: (id) => s.doc().rows.find((r) => r.id === id),
round: (id, n) => s.row(id).review.rounds[n - 1],
head: () => repo.g("rev-parse", "HEAD").trim(),
};
if (start) {
ok(s.run(["move", "6", "blocked", "--reason", "paused", "--op", "block-6-00001"], "darkwing"));
ok(s.run(["move", "9", "in-progress", "--op", "start-9-00001"], "darkwing"));
}
return s;
}
function ok(r, re) {
assert.equal(r.code, 0, `exit ${r.code}: ${r.out}${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;
}
function throwsCode(fn, code, re) {
assert.throws(fn, (err) => {
assert.equal(err.code, code, err.message);
if (re) assert.match(err.message, re);
return true;
});
}
const move9 = (op = "review-9-00001") => ["move", "9", "in-review", "--candidate", "HEAD", "--op", op];
const request = (op, id = 9) => ["review", "request", String(id), "--op", op];
// A commit on refs/heads/NAME: PARENT plus CHANGES ({path: text or null}),
// built in a private index so the working tree never moves.
function commitOn(repo, name, parent, changes) {
const env = { ...repo.env, GIT_INDEX_FILE: join(repo.base, `index-${name}`) };
const g = (args, input) => {
const r = spawnSync("git", ["-C", repo.root, ...args], { env, input, encoding: "utf8" });
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
return r.stdout.trim();
};
g(["read-tree", parent]);
for (const [path, text] of Object.entries(changes)) {
if (text === null) g(["update-index", "--force-remove", path]);
else g(["update-index", "--add", "--cacheinfo", `100644,${g(["hash-object", "-w", "--stdin"], text)},${path}`]);
}
const c = g(["commit-tree", g(["write-tree"]), "-p", parent, "-m", name]);
g(["update-ref", `refs/heads/${name}`, c]);
return c;
}
test("a request posts once as the requester; a retry sends nothing", async (t) => {
const s = await ready(t);
const head = s.head();
const r = ok(s.run(move9(), "darkwing"), /^ok review-9-00001 rev 3 row 9 in-progress→in-review round 1 on #1508; request review-9-00001 requesting$/m);
assert.match(r.out, /^ok review-9-00001\.outcome rev 4 row 9 round 1 request review-9-00001 posted comment 1000$/m);
const calls = s.calls();
assert.deepEqual(calls.map((c) => `${c.method} ${c.path}`), ["GET user", "POST repos/mosaicstack/stack/issues/1508/comments"]);
assert.ok(calls.every((c) => c.cred === s.tokens.darkwing));
const body = calls[1].body.body;
assert.ok(body.startsWith(`<!-- mosaic-queue-op: review-9-00001 -->\n<!-- mosaic-queue-round: row=9 round=1 candidate=${head} -->\n`), body);
assert.match(body, new RegExp(`queue review record 9 --verdict approve\\|changes --comment COMMENT_ID --candidate ${head}`));
assert.match(body, /- Reviewers: filbert\n/);
const round = s.round(9, 1);
assert.deepEqual(Object.keys(round), ["n", "op", "by", "at", "issue", "candidate", "request", "attempts", "duplicateRisk", "receipts"]);
assert.equal(round.request, "comment");
assert.deepEqual(round.attempts.map((a) => [a.op, a.by, a.state, a.comment, a.transport]), [
["review-9-00001", "darkwing", "posted", 1000, { outcome: "posted", status: 201, comment: 1000, detail: "created" }],
]);
assert.equal(s.doc().log.at(-1).op, "review-9-00001.outcome");
assert.match(readFileSync(s.repo.viewPath, "utf8"), /; in-review, round 1, request posted \|/);
const again = ok(s.run(move9(), "darkwing"), /\(already recorded at rev 3\)$/m);
assert.match(again.err, /request review-9-00001 is posted \(row 9 round 1 on #1508\), comment 1000; nothing was sent again/);
assert.equal(s.calls().length, 2);
no(s.run(request("request-9-0002"), "darkwing"), 2, /round 1 already has a posted request \(review-9-00001, comment 1000\)/);
no(s.run(["review", "outcome", "9"], "darkwing"), 4, /review takes request, resolve, abandon, record or verify-commit/);
throwsCode(() => s.mut({ verb: "review-outcome", op: "review-9-00001.outcome", args: { id: 9, attempt: "review-9-00001", outcome: "failed", status: 403, comment: null, detail: "x" }, by: "darkwing" }), 2, /records its own outcome/);
});
test("each transport answer maps to posted, failed or uncertain (8.9 step 3)", async (t) => {
const s = await ready(t);
s.rules([{ method: "POST", path: "comments$", http: 403, body: { message: "forbidden" } }]);
const first = no(s.run(move9(), "darkwing"), 1);
assert.match(first.out, /^ok review-9-00001\.outcome rev 4 row 9 round 1 request review-9-00001 failed$/m);
let n = 1;
const cases = [
[{ http: 400 }, 1, "failed", 400], [{ http: 401 }, 1, "failed", 401], [{ http: 404 }, 1, "failed", 404], [{ http: 422 }, 1, "failed", 422],
[{ http: 500 }, 3, "uncertain", 500], [{ http: 201, body: { nope: true } }, 3, "uncertain", 201], [{ http: 201, body: "not json" }, 3, "uncertain", 201],
[{ http: 200, body: { id: 5 } }, 3, "uncertain", 200], [{ fail: true }, 3, "uncertain", null],
];
for (const [rule, code, state, status] of cases) {
s.rules([{ method: "POST", path: "comments$", ...rule }]);
const op = `request-9-${String(++n).padStart(4, "0")}`;
const r = no(s.run(request(op), "darkwing"), code);
assert.match(r.out, new RegExp(`request ${op} ${state}$`, "m"));
const a = s.round(9, 1).attempts.at(-1);
assert.deepEqual([a.op, a.state, a.comment, a.transport.status], [op, state, null, status]);
assert.ok(!JSON.stringify(a.transport).includes("forbidden"));
if (state === "uncertain") {
no(s.run(["review", "abandon", "9", op, "--reason", "not posted", "--op", `abandon-${op}`], "sage"), 2, /re-run with --yes/);
no(s.run(["review", "abandon", "9", op, "--reason", "not posted", "--yes", "--op", `abandon-${op}`], "darkwing"), 2, /only a privileged actor/);
ok(s.run(["review", "abandon", "9", op, "--reason", "not posted", "--yes", "--op", `abandon-${op}`], "sage"), /uncertain→abandoned; a duplicate request comment may exist$/m);
}
}
assert.equal(s.round(9, 1).duplicateRisk, true);
s.rules([{ method: "POST", path: "comments$", sleepMs: 5000 }]);
const t0 = Date.now();
const late = s.mut({ verb: "review-request", op: "request-9-late1", args: { id: 9 }, by: "darkwing" }, { deadlineMs: 300 });
assert.ok(Date.now() - t0 < 4000, "the deadline killed the helper");
assert.equal(late.code, 3);
assert.deepEqual(s.round(9, 1).attempts.at(-1).transport, { outcome: "uncertain", status: null, comment: null, detail: "no answer before the deadline" });
ok(s.run(["review", "abandon", "9", "request-9-late1", "--reason", "timed out", "--yes", "--op", "abandon-late1"], "sage"));
s.rules([]);
ok(s.run(request("request-9-final"), "darkwing"), /request request-9-final posted comment \d+$/m);
assert.match(readFileSync(s.repo.viewPath, "utf8"), /; in-review, round 1, request posted \|/);
});
test("the pre-send checks: GET user must name the requester, under the deadline", async (t) => {
const s = await ready(t);
s.rules([{ method: "GET", path: "^user$", http: 200, body: { login: "dewey" } }]);
const r = no(s.run(move9(), "darkwing"), 1, /request review-9-00001 not sent: the token belongs to dewey, not darkwing/);
assert.match(r.out, /request review-9-00001 failed$/m);
assert.deepEqual(s.round(9, 1).attempts[0].transport, { outcome: "failed", status: null, comment: null, detail: "pre-send: the credential or account check failed" });
s.rules([{ method: "GET", path: "^user$", http: 200, body: { login: "Bad Name\n" } }]);
no(s.run(request("request-9-0002"), "darkwing"), 1, /the token belongs to an unexpected value, not darkwing/);
s.rules([{ method: "GET", path: "^user$", http: 401, body: { message: "bad token" } }]);
const unauth = no(s.run(request("request-9-0003"), "darkwing"), 1, /GET user answered HTTP 401/);
assert.ok(!unauth.err.includes("bad token"));
s.rules([{ method: "GET", path: "^user$", sleepMs: 5000 }]);
const slow = s.mut({ verb: "review-request", op: "request-9-0004", args: { id: 9 }, by: "darkwing" }, { deadlineMs: 300 });
assert.equal(slow.code, 1);
assert.match(slow.err.join("\n"), /GET user had no answer before the deadline/);
assert.equal(s.posts().length, 0);
// The lead posts as jarvis, with jarvis's file; a token for "sage" is refused.
s.rules([]);
ok(s.run(request("request-9-lead1"), "sage"), /request request-9-lead1 posted comment 1000$/m);
assert.equal(s.posts()[0].cred, s.tokens.jarvis);
assert.equal(s.calls().at(-2).cred, s.tokens.jarvis);
assert.equal(s.round(9, 1).attempts.at(-1).by, "sage");
});
test("the lead's request refuses a token for login sage", async (t) => {
const s = await ready(t);
s.rules([{ method: "POST", path: "comments$", http: 403 }]);
no(s.run(move9(), "darkwing"), 1);
s.rules([{ method: "GET", path: "^user$", http: 200, body: { login: "sage" } }]);
no(s.run(request("request-9-0002"), "sage"), 1, /the token belongs to sage, not jarvis/);
no(s.run(request("request-9-0003"), "sage", { MOSAIC_GITEA_CREDENTIAL_FILE: s.tokens.sage }), 1, /not jarvis's token file/);
assert.equal(s.posts().length, 1);
});
test("the credential file: the seat's own, 0600, no symlink, never the shared default", async (t) => {
const s = await ready(t);
const { credCheck, loginFor } = await import(pathToFileURL(join(s.repo.root, "packages/queue/src/review.mjs")).href);
assert.equal(loginFor("sage"), "jarvis");
assert.equal(loginFor("dewey"), "dewey");
const env = (p) => ({ HOME: s.repo.home, MOSAIC_GITEA_CREDENTIAL_FILE: p });
assert.equal(credCheck(env(s.tokens.darkwing), "darkwing"), null);
assert.equal(credCheck(env(s.tokens.jarvis), "sage"), null);
assert.match(credCheck({ HOME: s.repo.home }, "darkwing"), /is not set/);
assert.match(credCheck(env("fleet/agents/darkwing/secrets/gitea-mosaicstack-darkwing.token"), "darkwing"), /absolute path/);
const shared = join(s.repo.home, "secrets/mosaic.gitea.json");
mkdirSync(join(s.repo.home, "secrets"));
writeFileSync(shared, "{}", { mode: 0o600 });
assert.match(credCheck(env(shared), "darkwing"), /shared default file/);
assert.match(credCheck(env(s.tokens.dewey), "darkwing"), /not darkwing's token file/);
assert.match(credCheck(env(s.tokens.sage), "sage"), /not jarvis's token file/);
assert.match(credCheck(env(s.tokens.darkwing.replace("darkwing.token", "darkwing.tokenx")), "darkwing"), /not darkwing's token file/);
const gone = join(s.repo.base, "x/agents/darkwing/secrets/gitea-mosaicstack-darkwing.token");
assert.match(credCheck(env(gone), "darkwing"), /does not exist/);
chmodSync(s.tokens.rocko, 0o640);
assert.match(credCheck(env(s.tokens.rocko), "rocko"), /mode 0600/);
chmodSync(s.tokens.rocko, 0o400);
assert.match(credCheck(env(s.tokens.rocko), "rocko"), /mode 0600/);
// A symlinked file, and a symlinked directory leading to another seat's.
const link = join(s.repo.base, "l1/agents/filbert/secrets");
mkdirSync(link, { recursive: true });
symlinkSync(s.tokens.filbert, join(link, "gitea-mosaicstack-filbert.token"));
assert.match(credCheck(env(join(link, "gitea-mosaicstack-filbert.token")), "filbert"), /regular file, not a symlink/);
mkdirSync(join(s.repo.base, "l2/agents/filbert"), { recursive: true });
symlinkSync(join(s.repo.base, "fleet/agents/dewey/secrets"), join(s.repo.base, "l2/agents/filbert/secrets"));
writeFileSync(join(s.repo.base, "fleet/agents/dewey/secrets/gitea-mosaicstack-filbert.token"), "x", { mode: 0o600 });
assert.match(credCheck(env(join(s.repo.base, "l2/agents/filbert/secrets/gitea-mosaicstack-filbert.token")), "filbert"), /resolves outside filbert's secrets directory/);
// End to end: a refused file makes no call at all.
chmodSync(s.tokens.darkwing, 0o644);
no(s.run(move9(), "darkwing"), 1, /not sent: darkwing's token file must be mode 0600/);
no(s.run(request("request-9-0002"), "darkwing", { MOSAIC_GITEA_CREDENTIAL_FILE: s.tokens.dewey }), 1, /not darkwing's token file/);
no(s.run(request("request-9-0003"), "darkwing", { MOSAIC_GITEA_CREDENTIAL_FILE: "" }), 1, /is not set/);
assert.deepEqual(s.calls(), []);
});
test("an unresolved request blocks a new request, a new round, waiting-on-jason and done", async (t) => {
const s = await ready(t);
s.rules([{ method: "POST", path: "comments$", http: 502 }]);
no(s.run(move9(), "darkwing"), 3, /^Look on #1508 for a comment carrying <!-- mosaic-queue-op: review-9-00001 -->\.$/m);
s.rules([]);
const head = s.head();
no(s.run(request("request-9-0002"), "darkwing"), 2, /unresolved review request: review-9-00001 \(round 1, uncertain\)/);
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "77", "--candidate", head, "--op", "record-9-filbert1"], "filbert"));
no(s.run(["move", "9", "done", "--op", "done-9-000001"], "filbert"), 2, /unresolved review request: review-9-00001 \(round 1, uncertain\); resolve or abandon it before closing it/);
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /unresolved review request: review-9-00001 \(round 1, uncertain\); resolve or abandon it before moving it to waiting-on-jason/);
assert.equal(s.posts().length, 1);
// A POST that lands after the row reached waiting-on-jason: abandoned,
// approved and moved while it was in flight. Jason's close then refuses.
const s3 = await ready(t);
const head3 = s3.head();
const hook = (n) => {
if (n !== "posted") return;
s3.mut({ verb: "review-abandon", op: "abandon-9-0001", args: { id: 9, attempt: "review-9-00001", reason: "gave up" }, by: "sage", yes: true });
s3.mut({ verb: "review-record", op: "record-9-filb01", args: { id: 9, verdict: "approve", comment: 88, candidate: head3 }, by: "filbert" });
s3.mut({ verb: "move", op: "wait-9-000001", args: { id: 9, to: "waiting-on-jason" }, by: "darkwing" });
};
assert.equal(s3.mut({ verb: "move", op: "review-9-00001", args: { id: 9, to: "in-review", candidate: "HEAD" }, by: "darkwing" }, { hook }).code, 3);
assert.deepEqual([s3.row(9).state, s3.round(9, 1).attempts[0].state], ["waiting-on-jason", "conflict"]);
no(s3.run(["move", "9", "done", "--op", "done-9-000002"], "jason"), 2, /unresolved review request: review-9-00001 \(round 1, conflict\); resolve or abandon it before closing it/);
// Back to in-progress is allowed; the next round is not.
const s2 = await ready(t);
s2.rules([{ method: "POST", path: "comments$", http: 502 }]);
no(s2.run(move9(), "darkwing"), 3);
s2.rules([]);
ok(s2.run(["move", "9", "in-progress", "--op", "changes-9-0001"], "darkwing"));
no(s2.run(["move", "9", "in-review", "--candidate", "HEAD", "--op", "review-9-00002"], "darkwing"), 2, /unresolved review request: review-9-00001 \(round 1, uncertain\); resolve or abandon it before a new round/);
assert.equal(s2.posts().length, 1);
});
test("a same-op retry after a kill sends nothing, even with a stale view", async (t) => {
for (const step of ["pre-send", "posted", "outcome", "locked#2"]) {
const s = await ready(t);
const viewBefore = readFileSync(s.repo.viewPath);
const req = { verb: "move", op: "review-9-00001", args: { id: 9, to: "in-review", candidate: "HEAD" }, by: "darkwing" };
const r = spawnSync(process.execPath, [join(HERE, "fixtures", "kill-at.mjs"), join(s.repo.root, "packages/queue/src"), step, JSON.stringify(req)], {
cwd: s.repo.root, env: { ...s.repo.env, ...s.cred("darkwing") }, encoding: "utf8",
});
assert.equal(r.signal, "SIGKILL", `child did not die at ${step}: ${r.stdout}${r.stderr}`);
if (step === "locked#2") ok(cli(s.repo, ["unlock"]), /^removed queue lock \(dead/);
const posted = step === "pre-send" ? 0 : 1;
assert.equal(s.posts().length, posted, step);
assert.equal(s.round(9, 1).attempts[0].state, "requesting");
writeFileSync(s.repo.viewPath, viewBefore);
const calls = s.calls().length;
const again = no(s.run(move9(), "darkwing"), 3);
assert.match(again.out, /\(already recorded at rev 3\)$/m);
assert.match(again.err, /warning: view stale/);
assert.match(again.err, /request review-9-00001 is requesting \(row 9 round 1 on #1508\); nothing was sent again\.\nLook on #1508 for a comment carrying <!-- mosaic-queue-op: review-9-00001 -->\./);
assert.equal(s.calls().length, calls, step);
ok(cli(s.repo, ["render"]));
if (posted) {
ok(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "1000", "--op", "resolve-9-00001"], "darkwing"), /requesting→posted comment 1000$/m);
assert.equal(s.calls().at(-1).path, "repos/mosaicstack/stack/issues/comments/1000");
} else {
ok(s.run(["review", "abandon", "9", "review-9-00001", "--reason", "never sent", "--yes", "--op", "abandon-9-0001"], "sage"));
ok(s.run(request("request-9-0002"), "darkwing"), /posted comment 1000$/m);
}
assert.equal(s.posts().length, 1);
}
});
test("a held lock at the outcome exits 3 and names what the transport said", async (t) => {
const s = await ready(t);
let handle = null;
const hook = (n) => {
if (n === "outcome") handle = s.m.lock.acquire({ gitDir: s.repo.gitDir, io: s.m.io.realIo, op: "holder-op-1", verb: "move" });
};
throwsCode(() => s.mut({ verb: "move", op: "review-9-00001", args: { id: 9, to: "in-review", candidate: "HEAD" }, by: "darkwing" }, { hook, lockWaitMs: 200, lockStepMs: 50 }), 3,
/^ok review-9-00001 rev 3 row 9 in-progress→in-review round 1 on #1508; request review-9-00001 requesting\nuncertain review-9-00001: the transport said posted comment 1000; that outcome is not recorded: queue lock held by move holder-op-1/);
s.m.lock.releaseOrWarn(handle, s.m.io.realIo);
assert.equal(s.round(9, 1).attempts[0].state, "requesting");
no(s.run(move9(), "darkwing"), 3, /nothing was sent again/);
assert.equal(s.posts().length, 1);
});
test("late outcomes: after an abandon, and after a resolve with the same or another id", async (t) => {
const s = await ready(t);
const head = s.head();
const mv = (op) => ({ verb: "move", op, args: { id: 9, to: "in-review", candidate: "HEAD" }, by: "darkwing" });
// Abandoned while the POST was in flight, then the POST lands: conflict.
let hook = (n) => { if (n === "posted") s.mut({ verb: "review-abandon", op: "abandon-9-0001", args: { id: 9, attempt: "review-9-00001", reason: "gave up" }, by: "sage", yes: true }); };
const r1 = s.mut(mv("review-9-00001"), { hook });
assert.equal(r1.code, 3);
assert.match(r1.out.at(-1), /request review-9-00001 conflict \(transport posted\)$/);
let a = s.round(9, 1).attempts[0];
assert.deepEqual([a.state, a.comment, a.transport.comment, a.resolutions.map((x) => x.verb)], ["conflict", null, 1000, ["abandon"]]);
assert.equal(s.round(9, 1).duplicateRisk, true);
no(s.run(request("request-9-0002"), "darkwing"), 2, /unresolved review request: review-9-00001 \(round 1, conflict\)/);
ok(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "1000", "--op", "resolve-9-00001"], "darkwing"), /conflict→posted comment 1000$/m);
a = s.round(9, 1).attempts[0];
assert.deepEqual([a.state, a.comment, a.resolutions.map((x) => x.verb)], ["posted", 1000, ["abandon", "resolve"]]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "1000", "--op", "resolve-9-00002"], "darkwing"), 2, /is posted; only a requesting, uncertain or conflict request can be resolved/);
// Round 2: resolved with the id the POST returns, then the outcome agrees.
ok(s.run(["move", "9", "in-progress", "--op", "changes-9-0001"], "darkwing"));
hook = (n) => { if (n === "posted") s.mut({ verb: "review-resolve", op: "resolve-9-r2-01", args: { id: 9, attempt: "review-9-00002", comment: 1001 }, by: "darkwing" }); };
const r2 = s.mut(mv("review-9-00002"), { hook });
assert.equal(r2.code, 0);
assert.match(r2.out.at(-1), /request review-9-00002 posted comment 1001$/);
assert.deepEqual(s.round(9, 2).attempts[0].transport, { outcome: "posted", status: 201, comment: 1001, detail: "created" });
// Round 3: resolved with another comment that carries the markers, then
// the POST returns a different id: conflict.
ok(s.run(["move", "9", "in-progress", "--op", "changes-9-0002"], "darkwing"));
const marked = `<!-- mosaic-queue-op: review-9-00003 -->\n<!-- mosaic-queue-round: row=9 round=3 candidate=${head} -->\nposted by hand\n`;
s.rules([{ method: "GET", path: "comments/5555$", http: 200, body: { id: 5555, issue_url: "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/1508", body: marked, user: { login: "darkwing" } } }]);
hook = (n) => { if (n === "posted") s.mut({ verb: "review-resolve", op: "resolve-9-r3-01", args: { id: 9, attempt: "review-9-00003", comment: 5555 }, by: "darkwing" }); };
const r3 = s.mut(mv("review-9-00003"), { hook });
assert.equal(r3.code, 3);
a = s.round(9, 3).attempts[0];
assert.deepEqual([a.state, a.comment, a.transport.comment, a.resolutions.map((x) => [x.verb, x.comment])], ["conflict", null, 1002, [["resolve", 5555]]]);
assert.equal(s.round(9, 3).duplicateRisk, false);
// A retry of the logged resolve answers from the log and makes no call,
// though the attempt is unresolved again.
const before = s.calls().length;
ok(s.run(["review", "resolve", "9", "review-9-00003", "--comment", "5555", "--op", "resolve-9-r3-01"], "darkwing"), /already recorded/);
assert.equal(s.calls().length, before);
ok(s.run(["review", "resolve", "9", "review-9-00003", "--comment", "1002", "--op", "resolve-9-r3-02"], "darkwing"), /conflict→posted comment 1002$/m);
// Rounds 4 and 5: resolved by hand, then the POST answers 500 (the
// resolve stands) or 422 (a refusal contradicts it: conflict).
for (const [n, comment, http, code, state] of [[4, 5556, 500, 0, "posted"], [5, 5557, 422, 3, "conflict"]]) {
ok(s.run(["move", "9", "in-progress", "--op", `changes-9-000${n}`], "darkwing"));
const op = `review-9-0000${n}`;
const body = `<!-- mosaic-queue-op: ${op} -->\n<!-- mosaic-queue-round: row=9 round=${n} candidate=${head} -->\nposted by hand\n`;
s.rules([
{ method: "GET", path: `comments/${comment}$`, http: 200, body: { id: comment, issue_url: "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/1508", body, user: { login: "darkwing" } } },
{ method: "POST", path: "comments$", http },
]);
hook = (step) => { if (step === "posted") s.mut({ verb: "review-resolve", op: `resolve-9-r${n}-01`, args: { id: 9, attempt: op, comment }, by: "darkwing" }); };
const r = s.mut(mv(op), { hook });
assert.equal(r.code, code, `round ${n}`);
a = s.round(9, n).attempts[0];
assert.deepEqual([a.state, a.comment, a.transport.status], [state, state === "posted" ? comment : null, http], `round ${n}`);
}
assert.equal(s.posts().length, 5);
});
test("resolve checks the comment: issue, markers, round, candidate and author", async (t) => {
const s = await ready(t);
const head = s.head();
s.rules([{ method: "POST", path: "comments$", http: 500 }]);
no(s.run(move9(), "darkwing"), 3);
const good = { id: 7001, issue_url: "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/1508",
body: `intro\n<!-- mosaic-queue-op: review-9-00001 -->\n<!-- mosaic-queue-round: row=9 round=1 candidate=${head} -->\n`, user: { login: "darkwing" } };
const bad = [
[{ issue_url: good.issue_url.replace("1508", "1509") }, 2, /does not match request review-9-00001: the issue \(want #1508\)/],
[{ issue_url: `${good.issue_url}0` }, 2, /the issue/],
[{ body: good.body.replace("review-9-00001", "review-9-00009") }, 2, /the op marker for review-9-00001$/m],
[{ body: good.body.replace("round=1", "round=2") }, 2, /the round marker/],
[{ body: good.body.replace(head, "f".repeat(40)) }, 2, /the round marker \(row 9 round 1 candidate/],
[{ body: good.body.replace("-->\n<!--", "--> <!--") }, 2, /the op marker for review-9-00001, the round marker/],
[{ body: `x${good.body.replace("\n<!-- mosaic-queue-round", " <!-- mosaic-queue-round")}` }, 2, /the op marker for review-9-00001, the round marker/],
[{ user: { login: "dewey" } }, 2, /its author \(want darkwing\)/],
[{ id: 7002 }, 2, /its id/],
];
for (const [patch, code, re] of bad) {
s.rules([{ method: "GET", path: "comments/7001$", http: 200, body: { ...good, ...patch } }]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), code, re);
}
s.rules([{ method: "GET", path: "comments/7001$", http: 404 }]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), 2, /comment 7001 does not exist/);
s.rules([{ method: "GET", path: "comments/7001$", http: 500 }]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), 1, /answered HTTP 500/);
s.rules([{ method: "GET", path: "comments/7001$", fail: true }]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), 1, /failed or had no answer/);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing", { MOSAIC_GITEA_CREDENTIAL_FILE: s.tokens.dewey }), 2, /cannot check comment 7001: .*not darkwing's token file/);
const before = s.calls().length;
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "rocko"), 2, /^queue: row 9 is claimed by darkwing; only the owner \(darkwing\) or a privileged actor may resolve a request on row 9$/m);
assert.equal(s.calls().length, before, "a refused actor makes no request");
assert.equal(s.doc().revision, 4);
s.rules([{ method: "GET", path: "comments/7001$", http: 200, body: good }]);
ok(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), /uncertain→posted comment 7001$/m);
const calls = s.calls().length;
ok(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "darkwing"), /already recorded/);
assert.equal(s.calls().length, calls, "a recorded resolve checks nothing again");
const res = s.round(9, 1).attempts[0].resolutions;
assert.deepEqual(res.map((x) => [x.verb, x.by, x.comment, x.reason]), [["resolve", "darkwing", 7001, null]]);
});
test("the lead resolves a seat's request: the comment's author is the requester, fetched with the lead's token", async (t) => {
const s = await ready(t);
const head = s.head();
s.rules([{ method: "POST", path: "comments$", fail: true }]);
no(s.run(move9(), "darkwing"), 3);
const body = `<!-- mosaic-queue-op: review-9-00001 -->\n<!-- mosaic-queue-round: row=9 round=1 candidate=${head} -->\n`;
const comment = (login) => ({ id: 7001, issue_url: "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/1508", body, user: { login } });
s.rules([{ method: "GET", path: "comments/7001$", http: 200, body: comment("jarvis") }]);
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "sage"), 2, /its author \(want darkwing\)/);
s.rules([{ method: "GET", path: "comments/7001$", http: 200, body: comment("darkwing") }]);
ok(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "7001", "--op", "resolve-9-00001"], "sage"), /uncertain→posted comment 7001$/m);
assert.equal(s.calls().at(-1).cred, s.tokens.jarvis);
assert.deepEqual(s.round(9, 1).attempts[0].resolutions.map((x) => [x.by, x.comment]), [["sage", 7001]]);
});
test("validateRow checks a request round's shape, which every replayed entry must keep", async (t) => {
const s = await ready(t);
ok(s.run(move9(), "darkwing"));
const { queue } = s.m;
const d = s.doc();
const row9 = d.rows.find((r) => r.id === 9);
queue.validateRow(row9);
const forge = (fn) => {
const copy = structuredClone(row9);
fn(copy.review.rounds[0]);
return () => queue.validateRow(copy);
};
assert.throws(forge((r) => { r.duplicateRisk = true; }), /duplicateRisk is true exactly when an attempt was abandoned/);
assert.throws(forge((r) => { r.attempts[0].op = "someone-else-1"; }), /first attempt is the move that opened it/);
const rec = { reviewer: "filbert", op: "record-9-filb01", at: "2026-09-27T00:00:00.000Z", verdict: "approve", comment: 88, candidate: d.rows.find((r) => r.id === 9).review.rounds[0].candidate.digest };
queue.validateRow({ ...row9, review: { rounds: [{ ...row9.review.rounds[0], receipts: [rec] }] } });
assert.throws(forge((r) => { r.receipts = [rec, { ...rec, op: "record-9-filb02" }]; }), /a reviewer has one receipt per round/);
assert.throws(forge((r) => { r.attempts.push({ ...r.attempts[0] }); }), /a request round names an attempt twice/);
});
test("request, changes, a new candidate, approval: every round pinned; no review files", async (t) => {
const s = await ready(t);
const c1 = s.head();
ok(s.run(move9(), "darkwing"), /posted comment 1000$/m);
ok(cli(s.repo, ["next", "filbert"]), /^review row 9: Queue as data/m);
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "88", "--candidate", c1, "--op", "record-9-dark01"], "darkwing"), 2, /only a listed reviewer other than the owner/);
no(s.run(["review", "record", "9", "--verdict", "maybe", "--comment", "88", "--candidate", c1, "--op", "record-9-filb01"], "filbert"), 2, /approve or changes/);
no(s.run(["review", "record", "9", "--verdict", "changes", "--candidate", c1, "--op", "record-9-filb01"], "filbert"), 4, /--comment N is required/);
ok(s.run(["review", "record", "9", "--verdict", "changes", "--comment", "88", "--candidate", c1, "--op", "record-9-filb01"], "filbert"), /row 9 round 1 changes by filbert \(comment 88\)$/m);
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "89", "--candidate", c1, "--op", "record-9-filb02"], "filbert"), 2, /filbert already recorded a verdict for row 9 round 1/);
ok(cli(s.repo, ["next", "filbert"]), /^nothing$/m);
no(s.run(["move", "9", "done", "--op", "done-9-000001"], "filbert"), 2, /round 1 has no approval recorded by filbert/);
ok(s.run(["move", "9", "in-progress", "--op", "changes-9-0001"], "darkwing"));
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "89", "--candidate", c1, "--op", "record-9-filb02"], "filbert"), 2, /is in-progress; review record applies to in-review rows/);
writeFileSync(join(s.repo.root, "packages/fix.mjs"), "export {};\n");
s.repo.g("add", "packages/fix.mjs");
s.repo.g("commit", "-q", "-m", "fix", "--", "packages/fix.mjs");
const c2 = s.head();
ok(s.run(move9("review-9-00002"), "darkwing"), /round 2 on #1508; request review-9-00002 requesting$/m);
ok(cli(s.repo, ["next", "filbert"]), /^review row 9/m);
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "89", "--candidate", c1, "--op", "record-9-filb02"], "filbert"), 2, new RegExp(`candidate ${c1} is not round 2's candidate ${c2}`));
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "89", "--candidate", c2, "--op", "record-9-filb02"], "filbert"));
no(s.run(["move", "9", "done", "--evidence", `comment=89,round=2,candidate=${c2}`, "--op", "done-9-000001"], "filbert"), 2, /closes on its recorded verdicts; drop --evidence/);
no(s.run(["move", "9", "done", "--op", "done-9-000001"], "rocko"), 2, /only the gate owner \(filbert\) or a privileged actor/);
ok(s.run(["move", "9", "done", "--op", "done-9-000001"], "filbert"), /in-review→done round 2$/m);
const rounds = s.row(9).review.rounds;
assert.deepEqual(rounds.map((r) => [r.n, r.candidate.digest, r.attempts.map((a) => [a.op, a.state, a.comment]), r.receipts.map((x) => [x.reviewer, x.verdict, x.comment, x.candidate])]), [
[1, c1, [["review-9-00001", "posted", 1000]], [["filbert", "changes", 88, c1]]],
[2, c2, [["review-9-00002", "posted", 1001]], [["filbert", "approve", 89, c2]]],
]);
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "90", "--candidate", c2, "--op", "record-9-filb03"], "filbert"), 2, /row 9 is done; done rows never change/);
const untracked = s.repo.g("ls-files", "--others", "--exclude-standard").split("\n").filter(Boolean);
assert.deepEqual(untracked.filter((p) => p.startsWith("docs/plans/reviews/") || /^agents\/[^/]+\/work\//.test(p)), []);
assert.deepEqual(untracked, ["scripts/gitea-api.sh"]);
});
test("a row with no reviewers opens a round that posts nothing", async (t) => {
const s = await ready(t);
ok(s.run(["set", "9", "reviewers", "none", "--op", "reviewers-9-none"], "sage"));
const r = ok(s.run(move9(), "darkwing"), /in-progress→in-review round 1 on #1508$/m);
assert.doesNotMatch(r.out, /request/);
assert.deepEqual(Object.keys(s.round(9, 1)), ["n", "op", "by", "at", "issue", "candidate", "request"]);
assert.equal(s.round(9, 1).request, "none");
no(s.run(request("request-9-0002"), "darkwing"), 2, /posts no request \(the row had no reviewers when it opened\)/);
assert.deepEqual(s.calls(), []);
// The other way round: a request round whose reviewers are then removed
// doesn't close on no approvals.
const s2 = await ready(t);
ok(s2.run(move9(), "darkwing"));
ok(s2.run(["set", "9", "reviewers", "none", "--op", "reviewers-9-none"], "sage"));
no(s2.run(["move", "9", "done", "--op", "done-9-000001"], "sage"), 2, /row 9 lists no reviewers now; a privileged actor sets them before it closes/);
});
test("verify-commit: a prospective tree must hold exactly the candidate's paths", async (t) => {
const s = await ready(t);
const base = s.head();
const cand = commitOn(s.repo, "cand", base, { "packages/x.mjs": "export const x = 1;\n", "agents/rocko/.keep": null });
ok(s.run(["move", "9", "in-review", "--candidate", "cand", "--op", "review-9-00001"], "darkwing"));
const other = commitOn(s.repo, "other", base, { "docs/other.md": "other\n" });
const good = commitOn(s.repo, "good", other, { "packages/x.mjs": "export const x = 1;\n", "agents/rocko/.keep": null });
ok(cli(s.repo, ["review", "verify-commit", "9", "good"]), /^ok row 9 round 1: good matches the commit candidate \(2 paths\)$/m);
const changed = commitOn(s.repo, "changed", good, { "packages/x.mjs": "export const x = 2;\n" });
no(cli(s.repo, ["review", "verify-commit", "9", "changed"]), 2, /changed does not match row 9 round 1's candidate:\npackages\/x.mjs: content differs$/m);
const exec = spawnSync("git", ["-C", s.repo.root, "commit-tree", s.repo.g("rev-parse", "good^{tree}").trim(), "-p", good, "-m", "exec"], { env: s.repo.env, encoding: "utf8" });
assert.equal(exec.status, 0, exec.stderr);
const execEnv = { ...s.repo.env, GIT_INDEX_FILE: join(s.repo.base, "index-exec") };
const gx = (...a) => { const r = spawnSync("git", ["-C", s.repo.root, ...a], { env: execEnv, encoding: "utf8" }); assert.equal(r.status, 0, r.stderr); return r.stdout.trim(); };
gx("read-tree", good);
const blob = gx("rev-parse", "good:packages/x.mjs");
gx("update-index", "--cacheinfo", `100755,${blob},packages/x.mjs`);
gx("update-ref", "refs/heads/execbit", gx("commit-tree", gx("write-tree"), "-p", good, "-m", "execbit"));
no(cli(s.repo, ["review", "verify-commit", "9", "execbit"]), 2, /^packages\/x.mjs: mode differs$/m);
const kept = commitOn(s.repo, "kept", good, { "agents/rocko/.keep": "" });
no(cli(s.repo, ["review", "verify-commit", "9", "kept"]), 2, /agents\/rocko\/.keep: deleted in the candidate, present here/);
no(cli(s.repo, ["review", "verify-commit", "9", "other"]), 2, /packages\/x.mjs: missing\nagents\/rocko\/.keep: deleted|agents\/rocko\/.keep: deleted in the candidate, present here\npackages\/x.mjs: missing/);
no(cli(s.repo, ["review", "verify-commit", "9", "nope"]), 2, /nope is not a commit or tree here/);
no(cli(s.repo, ["review", "verify-commit", "11", "good"]), 2, /row 11 has no review round/);
// A manifest candidate on row 6.
ok(s.run(["move", "6", "in-progress", "--op", "unblock-6-0001"], "darkwing"));
const digest = spawnSync("sha256sum", { input: "export const x = 1;\n", encoding: "utf8" }).stdout.slice(0, 64);
const manifest = join(s.repo.base, "cand.sha256");
writeFileSync(manifest, `${digest} packages/x.mjs\n`);
ok(s.run(["move", "6", "in-review", "--candidate", manifest, "--op", "review-6-00001"], "darkwing"), /on #1511; request review-6-00001 requesting$/m);
assert.match(s.posts().at(-1).body.body, new RegExp(`\`\`\`text\n${digest} packages/x.mjs\n\`\`\``));
ok(cli(s.repo, ["review", "verify-commit", "6", "good"]), /matches the manifest candidate \(1 paths\)/);
no(cli(s.repo, ["review", "verify-commit", "6", "changed"]), 2, /packages\/x.mjs: content differs/);
no(cli(s.repo, ["review", "verify-commit", "6", "HEAD"]), 2, /packages\/x.mjs: missing/);
});
test("semantics: v1 entries replay as before; review entries need v2", async (t) => {
const s = await ready(t);
const { queue } = s.m;
ok(s.run(move9(), "darkwing"));
const d = s.doc();
queue.loadDoc(Buffer.from(queue.serialize(d)));
// The same move under semantics 1 opens a round with no request.
const mv = d.log.find((e) => e.op === "review-9-00001");
const pre = queue.replay(d.log.slice(0, mv.rev));
const v1 = queue.applyEntry(pre, { ...mv, semantics: 1 }, queue.resolvedFromResult("move", mv.args, mv.result));
assert.equal(v1.state.rows.get(9).review.rounds[0].request, "none");
assert.equal(v1.result.receipt, "ok review-9-00001 rev 3 row 9 in-progress→in-review round 1 on #1508");
// A log written under semantics 1 with no review entries still loads.
const old = { ...d, revision: 2, rows: queue.rowsArray(pre), log: d.log.slice(0, 3).map((e) => ({ ...e, semantics: 1 })) };
queue.loadDoc(Buffer.from(queue.serialize(old)));
// Relabelling the request move as semantics 1 breaks the replay.
const forged = { ...d, log: d.log.map((e) => (e.op === mv.op ? { ...e, semantics: 1 } : e)) };
assert.throws(() => queue.loadDoc(Buffer.from(queue.serialize(forged))), /does not replay|differs from its replay|needs semantics 2|rows do not equal/);
const outcome = d.log.at(-1);
assert.throws(() => queue.loadDoc(Buffer.from(queue.serialize({ ...d, log: [...d.log.slice(0, -1), { ...outcome, semantics: 1 }] }))), /review-outcome needs semantics 2/);
assert.throws(() => queue.loadDoc(Buffer.from(queue.serialize({ ...d, log: [...d.log.slice(0, -1), { ...outcome, op: "review-9-00002.outcome" }] }))), /op must be its attempt's op plus \.outcome/);
assert.throws(() => queue.loadDoc(Buffer.from(queue.serialize({ ...d, log: d.log.map((e, i) => (i === 1 ? { ...e, semantics: 3 } : e)) }))), /semantics 3 is not 1 to 2/);
// An attempt takes one outcome, even one that agrees with it.
const full = queue.replay(d.log);
const again = { ...outcome, rev: full.revision + 1 };
assert.throws(() => queue.applyEntry(full, again, {}), /request review-9-00001 already has its outcome/);
// review-outcome is the one entry a done row takes.
const at = "2026-09-27T00:00:00.000Z";
// The request was abandoned and the row closed before the POST's answer came.
const entry = (st, verb, op, args, by = "darkwing") => ({ rev: st.revision + 1, op, verb, args: queue.canonArgs(verb, args), by, at, semantics: 2, result: null, viewSha: null });
const mid = queue.replay(d.log.slice(0, -1));
assert.equal(mid.rows.get(9).review.rounds[0].attempts[0].state, "requesting");
const gaveUp = queue.applyEntry(mid, entry(mid, "review-abandon", "abandon-9-0001", { id: 9, attempt: "review-9-00001", reason: "no answer" }, "sage"), {}).state;
const done = new Map(gaveUp.rows);
done.set(9, { ...gaveUp.rows.get(9), state: "done", claim: null });
const st = { ...gaveUp, rows: done };
const late = queue.applyEntry(st, entry(st, "review-outcome", "review-9-00001.outcome", { id: 9, attempt: "review-9-00001", outcome: "posted", status: 201, comment: 1000, detail: "created" }), {});
const a = late.state.rows.get(9).review.rounds[0].attempts[0];
assert.deepEqual([late.state.rows.get(9).state, a.state, a.transport.comment], ["done", "conflict", 1000]);
const lateOk = (verb, op, args, by) => queue.applyEntry(st, entry(st, verb, op, args, by), {});
assert.throws(() => lateOk("review-resolve", "resolve-9-00001", { id: 9, attempt: "review-9-00001", comment: 5 }), /done rows never change/);
assert.throws(() => queue.applyEntry(mid, entry(mid, "review-outcome", "review-9-00001.outcome", { id: 9, attempt: "review-9-00001", outcome: "failed", status: 403, comment: null, detail: "x" }, "sage"), {}), /only darkwing, who made request review-9-00001, records its outcome/);
assert.throws(() => queue.canonArgs("review-outcome", { id: 9, attempt: "review-9-00001", outcome: "failed", status: 500, comment: null, detail: "x" }), /a failed outcome's status is one of 400, 401, 403, 404, 422/);
assert.throws(() => queue.canonArgs("review-outcome", { id: 9, attempt: "review-9-00001", outcome: "posted", status: 201, comment: null, detail: "x" }), /posted exactly when HTTP 201 returned a comment id/);
assert.throws(() => queue.canonArgs("review-outcome", { id: 9, attempt: "review-9-00001", outcome: "posted", status: 200, comment: 4, detail: "x" }), /posted exactly when HTTP 201/);
});
test("the owner records no verdict, even as a listed reviewer", async (t) => {
const s = await ready(t);
ok(s.run(move9(), "darkwing"));
ok(s.run(["set", "9", "reviewers", "filbert,darkwing", "--op", "reviewers-9-self"], "sage"));
no(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "1000", "--candidate", s.head(), "--op", "record-9-self-01"], "darkwing"), 2, /only a listed reviewer other than the owner may record a verdict on row 9/);
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "1000", "--candidate", s.head(), "--op", "record-9-filbert"], "filbert"));
});
test("a request comment over the length limit is not sent", async (t) => {
const s = await ready(t);
ok(s.run(["move", "6", "in-progress", "--op", "unblock-6-0001"], "darkwing"));
const lines = Array.from({ length: 700 }, (_, i) => `${"a".repeat(64)} packages/generated/file-${String(i).padStart(4, "0")}-${"x".repeat(20)}.mjs`);
const manifest = join(s.repo.base, "big.sha256");
writeFileSync(manifest, `${lines.join("\n")}\n`);
const r = no(s.run(["move", "6", "in-review", "--candidate", manifest, "--op", "review-6-00001"], "darkwing"), 1, /request review-6-00001 not sent: the comment would be longer than the limit/);
assert.match(r.out, /request review-6-00001 failed$/m);
assert.equal(s.round(6, 1).attempts[0].transport.detail, "pre-send: the request comment is too long");
assert.equal(s.posts().length, 0);
});
test("a late POST on a closed row leaves a conflict nothing can resolve, and resolve asks nothing", async (t) => {
const s = await ready(t);
const head = s.head();
// Abandoned, approved and closed while the POST was in flight.
const hook = (n) => {
if (n !== "posted") return;
s.mut({ verb: "review-abandon", op: "abandon-9-0001", args: { id: 9, attempt: "review-9-00001", reason: "gave up" }, by: "sage", yes: true });
s.mut({ verb: "review-record", op: "record-9-filb01", args: { id: 9, verdict: "approve", comment: 88, candidate: head }, by: "filbert" });
s.mut({ verb: "move", op: "done-9-000001", args: { id: 9, to: "done" }, by: "filbert" });
};
const r = s.mut({ verb: "move", op: "review-9-00001", args: { id: 9, to: "in-review", candidate: "HEAD" }, by: "darkwing" }, { hook });
assert.equal(r.code, 3);
const a = s.round(9, 1).attempts[0];
assert.deepEqual([s.row(9).state, a.state, a.transport.comment], ["done", "conflict", 1000]);
const calls = s.calls().length;
no(s.run(["review", "resolve", "9", "review-9-00001", "--comment", "1000", "--op", "resolve-9-00001"], "darkwing"), 2, /row 9 is done; done rows never change/);
assert.equal(s.calls().length, calls, "a done row's resolve fetches nothing");
assert.equal(s.posts().length, 1);
});
test("a Jason-gated row reaches waiting-on-jason only on every reviewer's approval", async (t) => {
const s = await ready(t);
const head = s.head();
ok(s.run(["set", "9", "gate-owner", "jason", "--op", "gate-9-jason"], "sage"));
ok(s.run(["set", "9", "reviewers", "filbert,rocko", "--op", "reviewers-9-two"], "sage"));
ok(s.run(move9(), "darkwing"), /posted comment 1000$/m);
assert.deepEqual(s.round(9, 1).receipts, []);
no(s.run(["move", "9", "done", "--op", "done-9-000001"], "darkwing"), 2, /gate is Jason's; it goes through waiting-on-jason/);
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /row 9 round 1 has no approval recorded by filbert, rocko$/m);
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "sage"), 2, /no approval recorded by filbert, rocko$/m);
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "88", "--candidate", head, "--op", "record-9-filb01"], "filbert"));
ok(s.run(["review", "record", "9", "--verdict", "changes", "--comment", "89", "--candidate", head, "--op", "record-9-rock01"], "rocko"));
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /no approval recorded by rocko$/m);
// Changes go back to in-progress, as before.
ok(s.run(["move", "9", "in-progress", "--op", "changes-9-0001"], "darkwing"));
ok(s.run(move9("review-9-00002"), "darkwing"), /posted comment 1001$/m);
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "91", "--candidate", head, "--op", "record-9-rock02"], "rocko"));
// Filbert's round 1 approval doesn't carry into round 2.
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /row 9 round 2 has no approval recorded by filbert$/m);
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "90", "--candidate", head, "--op", "record-9-filb02"], "filbert"));
ok(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), /in-review→waiting-on-jason$/m);
ok(s.run(["move", "9", "done", "--op", "done-9-000001"], "jason"), /waiting-on-jason→done$/m);
// Reviewers removed after the round opened: waiting-on-jason refuses
// until a privileged actor sets them.
const s2 = await ready(t);
ok(s2.run(["set", "9", "gate-owner", "jason", "--op", "gate-9-jason"], "sage"));
ok(s2.run(move9(), "darkwing"));
ok(s2.run(["set", "9", "reviewers", "none", "--op", "reviewers-9-none"], "sage"));
no(s2.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /row 9 lists no reviewers now; a privileged actor sets them before it moves to waiting-on-jason/);
});