Files
stack/packages/ledger/tests/queue-checks.test.mjs
T
jason.woltjeandClaude Opus 5.5 fd72d26899 feat(ledger): Piece E, queue section in the weekly ledger (row 13, #1508)
The ledger prints a queue section above the weekly table. It checks four
things:
- open issues named by done rows;
- owner registrations for active rows;
- closed issues for done rows;
- the age of required rows.
The result is fail, incomplete or reduced pass. It uses its own Gitea
budget of the open list plus at most 10 lookups. A full open page counts
only while an issue in some row's closes has no known state (lead
decision 40). T3 seats are exempt per run with --unsupported-runtime.
The weekly routine is in packages/ledger/README.md.

Built by Darkwing (build.patch ab1f12ca, manifest 0b20bbca). Filbert
reviewed it: round 1 81f26f2e asked for changes (C1, ISO requiredSince
never aged); round 2 ce8ce150 approved. Also carries Filbert's plan
amendment for decision 40 (68a25ffe).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 11:33:44 -05:00

450 lines
31 KiB
JavaScript

// The ledger's queue section (Piece E, #1508; plan 8.10). The checks run
// in-process on fixture rows with an injected clock and pid probe. The CLI
// runs in a scratch repository whose queue.json the real queue writer made,
// against a fake gitea-api.sh. No test reads a real credential, registration
// or config: every run sets HOME and MOSAIC_CONFIG to temporary paths.
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { queueChecks, formatQueue, issueStates, readQueue, readOpenIssues, lookupIssue, classifySeat, pidAlive, protectedChanges, LOOKUP_BUDGET } from '../src/queue-checks.mjs';
import { writeRegistration } from '../../seat/src/seat.mjs';
import { scratchRepo, genesisCommitted, mapText, MAP_ROWS, cli as queueCli } from '../../queue/tests/helpers.mjs';
const PACKAGES = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const DAY = 86400000;
const NOW = Date.parse('2026-09-28T09:00:00Z');
const ROOT = '/srv/checkout';
const row = (id, over = {}) => ({ id, piece: `Row ${id}`, owner: 'alice', issues: [], closes: [], state: 'briefed', required: false, requiredSince: null, ...over });
const queue = (rows, genesisAt = '2026-09-27T01:00:00Z') => ({ revision: 7, canonicalRoot: ROOT, genesisAt, rows });
const states = (map, { lookups = 0, openListFull = false } = {}) => ({
states: new Map(Object.entries(map).map(([n, state]) => [Number(n), { state, detail: state === 'unknown' ? 'lookup failed' : 'lookup' }])),
lookups, openListFull,
});
// Seats directory with one registration per entry: [seat, pid, root].
function seatsWith(t, entries) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'ledger-seats-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
for (const [seat, pid, root = ROOT] of entries) {
writeRegistration(dir, { version: 1, seat, project: 'checkout', task: '', workspace: root, tmux: null, harness: 'pi', startedAt: '2026-09-27T00:00:00.000Z',
pid, sessionsDir: path.join(root, '.pi/state', seat, 'sessions'), seatDir: path.join(root, 'agents', seat), launchScript: path.join(root, 'agents', seat, 'launch.sh'), layout: 'repo', updatedAt: null });
}
return dir;
}
const run = (rows, { issues = states({}), seats = null, exempt = [], genesisAt, isPidAlive = () => true } = {}) =>
queueChecks(queue(rows, genesisAt), { issues, seats, exempt: new Set(exempt), now: NOW, isPidAlive });
const ids = list => list.map(f => [f.check, f.row, f.issue]);
test('a done row whose closing issue is open is a violation; a row that is not done is not', () => {
const rows = [row(1, { state: 'done', issues: [101], closes: [101] }), row(2, { state: 'briefed', issues: [102], closes: [102] })];
const open = run(rows, { issues: states({ 101: 'open', 102: 'open' }) });
assert.deepEqual(ids(open.violations), [['issue-open', 1, 101]]);
assert.equal(open.result, 'fail');
const closed = run(rows, { issues: states({ 101: 'closed', 102: 'open' }) });
assert.deepEqual([closed.violations, closed.undecided, closed.dispositions], [[], [], []]);
assert.equal(closed.result, 'reduced pass');
});
test('an issue several rows close is expected closed only once all of them are done', () => {
const rows = [row(1, { state: 'done', issues: [200], closes: [200] }), row(2, { state: 'in-progress', owner: 'bob', issues: [200], closes: [200] })];
const exempt = ['bob'];
assert.deepEqual(ids(run(rows, { issues: states({ 200: 'open' }), exempt }).violations), []);
const early = run(rows, { issues: states({ 200: 'closed' }), exempt });
assert.deepEqual(ids(early.dispositions), [['issue-closed-early', 2, 200]]);
assert.match(early.dispositions[0].message, /^#200 is closed, but row 2 that closes it is not done$/);
assert.equal(early.result, 'reduced pass', 'a disposition is printed, not counted');
const both = [rows[0], { ...rows[1], state: 'done' }];
assert.deepEqual(ids(run(both, { issues: states({ 200: 'open' }) }).violations), [['issue-open', 1, 200], ['issue-open', 2, 200]]);
// Rows 9 to 12 name #1508 without closing it: done while it is open is fine.
const named = [row(9, { state: 'done', issues: [1508], closes: [] }), row(13, { state: 'briefed', issues: [1508], closes: [1508] })];
const r = run(named, { issues: states({ 1508: 'open' }) });
assert.deepEqual([r.violations, r.result], [[], 'reduced pass']);
});
test('closure needs positive evidence: unknown is undecided, and so is a skipped or short issue check', () => {
const rows = [row(1, { state: 'done', issues: [101], closes: [101] })];
const unknown = run(rows, { issues: states({ 101: 'unknown' }) });
assert.deepEqual([ids(unknown.undecided), unknown.result], [[['issue-unknown', 1, 101]], 'incomplete']);
assert.match(unknown.undecided[0].message, /whether #101 is closed is unknown \(lookup failed\)/);
const skipped = run(rows, { issues: null });
assert.deepEqual([ids(skipped.undecided), skipped.result, skipped.issueChecks], [[['issues-not-run', null, null]], 'incomplete', { run: false }]);
// A full page is undecided only while some issue a row closes is unknown.
const full = run(rows, { issues: states({ 101: 'closed' }, { openListFull: true }) });
assert.deepEqual([full.undecided, full.result], [[], 'reduced pass']);
const short = run([...rows, row(2, { issues: [102], closes: [102] })], { issues: states({ 101: 'closed', 102: 'unknown' }, { openListFull: true }) });
assert.deepEqual([ids(short.undecided), short.result], [[['open-list-full', null, null]], 'incomplete']);
assert.match(short.undecided[0].message, /full page of 50, and #102 has no known state$/);
});
test('each owner of an in-progress or in-review row gets one liveness class', t => {
const other = '/srv/another-clone';
const seats = seatsWith(t, [['present', 101], ['gone', 102], ['nopid', null], ['elsewhere', 103, other], ['exempted', 104]]);
mkdirSync(path.join(seats, 'repo', 'broken'), { recursive: true });
writeFileSync(path.join(seats, 'repo', 'broken', 'registration.json'), '{not json');
const owners = ['present', 'gone', 'nopid', 'elsewhere', 'exempted', 'broken', 'absent'];
const rows = [...owners.map((owner, i) => row(i + 1, { owner, state: i % 2 ? 'in-review' : 'in-progress' })),
row(20, { owner: 'idle', state: 'briefed' }), row(21, { owner: 'finished', state: 'done' }), row(22, { owner: 'present', state: 'in-progress' })];
const r = run(rows, { seats, exempt: ['exempted'], isPidAlive: pid => pid !== 102 });
assert.deepEqual(r.liveness.seats.map(s => [s.seat, s.class, s.rows]), [
['absent', 'missing', [7]], ['broken', 'invalid', [6]], ['elsewhere', 'missing', [4]], ['exempted', 'exempt', [5]],
['gone', 'pid-gone', [2]], ['nopid', 'pid-unknown', [3]], ['present', 'pid-present', [1, 22]],
]);
assert.deepEqual(r.liveness.counts, { 'pid-present': 1, exempt: 1, 'pid-unknown': 1, missing: 2, invalid: 1, 'pid-gone': 1 });
assert.deepEqual(ids(r.violations).sort(), [['owner-invalid', 6, null], ['owner-missing', 4, null], ['owner-missing', 7, null], ['owner-pid-gone', 2, null]].sort());
assert.deepEqual(ids(r.undecided), [['owner-pid-unknown', 3, null]]);
assert.match(r.violations.find(f => f.row === 4).message, /another checkout/);
// Without a readable config there is no seats directory: invalid, never missing.
assert.deepEqual(classifySeat('present', { seats: null, canonicalRoot: ROOT, exempt: new Set() }).class, 'invalid');
// Nothing active, nothing checked: exempt seats with no rows add no count.
const idle = run([row(1)], { seats, exempt: ['present'] });
assert.deepEqual([idle.liveness.seats, idle.liveness.exempt, idle.result], [[], ['present'], 'reduced pass']);
});
test('a required row not done after 14 days is a violation; a legacy row uses genesis as its lower bound', () => {
const since = days => new Date(NOW - days * DAY).toISOString().slice(0, 10);
const rows = [
row(1, { required: true, requiredSince: since(15) }), row(2, { required: true, requiredSince: since(14) }),
row(3, { required: true, requiredSince: since(40), state: 'done' }), row(4, { required: false, state: 'waiting-on-jason' }),
];
const r = run(rows);
assert.deepEqual(ids(r.violations), [['age', 1, null]]);
assert.match(r.violations[0].message, /^row 1 "Row 1" is required and not done, 15 days since \d{4}-\d{2}-\d{2}$/);
const legacy = [row(5, { required: true, requiredSince: 'unknown', state: 'waiting-on-jason' })];
const old = run(legacy, { genesisAt: new Date(NOW - 20 * DAY).toISOString() });
assert.deepEqual(ids(old.violations), [['age', 5, null]]);
assert.match(old.violations[0].message, /age ≥ 20 days \(legacy lower bound\)$/);
const edge = run(legacy, { genesisAt: new Date(NOW - 14 * DAY).toISOString() });
assert.deepEqual([ids(edge.violations), ids(edge.undecided)], [[], [['age-unknown', 5, null]]], 'exactly 14 days is not over 14');
const young = run(legacy, { genesisAt: new Date(NOW - 3 * DAY).toISOString() });
assert.deepEqual([ids(young.undecided), young.result], [[['age-unknown', 5, null]], 'incomplete']);
});
test('an ISO requiredSince, as `set required` writes it, ages from its UTC day; one that does not parse is a violation', () => {
const at = ms => new Date(ms).toISOString();
const rows = [
row(1, { required: true, requiredSince: at(NOW - 15 * DAY) }), row(2, { required: true, requiredSince: at(NOW - 14 * DAY) }),
// 23:59Z fifteen days back is still that day: 15 whole days, not 14.
row(3, { required: true, requiredSince: '2026-09-13T23:59:59.999Z' }),
];
const r = run(rows);
assert.deepEqual(ids(r.violations), [['age', 1, null], ['age', 3, null]]);
assert.match(r.violations[0].message, /^row 1 "Row 1" is required and not done, 15 days since 2026-09-13T09:00:00\.000Z$/);
// The queue validator checks the shape, not the calendar.
const bad = run([row(4, { required: true, requiredSince: '2026-13-01T00:00:00.000Z' })]);
assert.deepEqual([ids(bad.violations), bad.result], [[['age-invalid', 4, null]], 'fail']);
assert.match(bad.violations[0].message, /^row 4 is required and not done, and its requiredSince "2026-13-01T00:00:00\.000Z" is not a date$/);
});
test('the text section always ends in a count and a result, and never prints a full pass', () => {
const clean = formatQueue(run([row(1)]));
assert.deepEqual(clean.slice(-4), ['queue issue checks: open list, 0 lookups', 'protected changes in range: 0 (not checks; confirm the actors)', 'queue: 0 violations; result reduced pass', '']);
assert.ok(clean.includes('liveness: 0 pid-present (unverified), 0 exempt, 0 pid-unknown, 0 missing, 0 invalid, 0 pid-gone'));
const bad = formatQueue(run([row(1, { state: 'done', issues: [101], closes: [101] })], { issues: states({ 101: 'open' }, { lookups: 1 }) }));
assert.ok(bad.includes('violation issue-open row 1 #101: row 1 is done and every row that closes #101 is done, but #101 is open'));
assert.deepEqual(bad.slice(-4), ['queue issue checks: open list, 1 lookup', 'protected changes in range: 0 (not checks; confirm the actors)', 'queue: 1 violation; result fail', '']);
assert.deepEqual(formatQueue({ checked: false }), ['Queue: not checked (--no-queue)']);
assert.ok(formatQueue(run([row(1)], { issues: null })).includes('queue issue checks: not run'));
});
test('pidAlive: a running pid is present, an exited one is gone, and EPERM still means present', () => {
assert.equal(pidAlive(process.pid), true);
const child = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' });
assert.equal(pidAlive(Number(child.stdout)), false);
// pid 1 belongs to root: signal 0 from another user fails with EPERM.
if (process.getuid() !== 0) assert.equal(pidAlive(1), true);
});
// A fake helper that answers the open list from OPEN, an issue from
// ISSUE_DIR/N.json (exit 1 when absent, like a 404), and logs every call.
function fakeTool(t) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'ledger-fake-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const tool = path.join(dir, 'gitea-api.sh');
writeFileSync(tool, `#!/usr/bin/env node
const fs = require('fs'), p = process.argv[3];
fs.appendFileSync(process.env.CALLS, JSON.stringify(process.argv.slice(2)) + '\\n');
if (process.env.API_FAIL) { console.error('secret must not escape'); process.exit(Number(process.env.API_FAIL)); }
if (p.includes('state=open')) { if (process.env.OPEN_FAIL) process.exit(Number(process.env.OPEN_FAIL)); process.stdout.write(fs.readFileSync(process.env.OPEN, 'utf8')); }
else if (/issues\\/\\d+$/.test(p)) { const f = process.env.ISSUE_DIR + '/' + p.split('/').pop() + '.json'; if (!fs.existsSync(f)) process.exit(1); process.stdout.write(fs.readFileSync(f, 'utf8')); }
else process.stdout.write(fs.readFileSync(process.env.ISSUES, 'utf8'));
`);
chmodSync(tool, 0o755);
mkdirSync(path.join(dir, 'issues'));
const env = { CALLS: path.join(dir, 'calls.jsonl'), OPEN: path.join(dir, 'open.json'), ISSUE_DIR: path.join(dir, 'issues'), ISSUES: path.join(dir, 'metric.json') };
writeFileSync(env.CALLS, ''); writeFileSync(env.OPEN, '[]'); writeFileSync(env.ISSUES, '[]');
const issue = (n, over = {}) => writeFileSync(path.join(env.ISSUE_DIR, `${n}.json`), JSON.stringify({ number: n, title: `Issue ${n}`, state: 'closed', ...over }));
const calls = () => readFileSync(env.CALLS, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)[1]);
return { dir, tool, env, issue, calls };
}
const openIssue = n => ({ number: n, title: `Issue ${n}`, state: 'open' });
test('issue states: open list first, then the metric page, then at most 10 lookups', t => {
const f = fakeTool(t);
Object.assign(process.env, f.env);
t.after(() => { for (const k of Object.keys(f.env)) delete process.env[k]; });
writeFileSync(f.env.OPEN, JSON.stringify([openIssue(1), { ...openIssue(2), pull_request: {} }]));
const closes = [1, 2, 3, ...Array.from({ length: 12 }, (_, i) => 10 + i)];
const rows = closes.map(n => row(n, { issues: [n], closes: [n] }));
f.issue(10); f.issue(11, { state: 'open' }); f.issue(12, { pull_request: { merged: true } }); f.issue(13, { number: 99 });
// #10 is on the metric page but not closed there, so it still needs a lookup.
// #11 was reopened: a closed_at without state closed is no evidence either,
// and neither is state closed without a closed_at (#12).
const metric = [{ number: 3, state: 'closed', closed_at: '2026-09-20T00:00:00Z' }, { number: 10, state: 'open', closed_at: null },
{ number: 11, state: 'open', closed_at: '2026-09-21T00:00:00Z' }, { number: 12, state: 'closed', closed_at: null }];
const r = issueStates('/', rows, metric, f.tool);
const state = n => r.states.get(n);
assert.deepEqual([state(1).state, state(3), state(10), state(11).state], ['open', { state: 'closed', detail: 'metric page' }, { state: 'closed', detail: 'lookup' }, 'open']);
assert.deepEqual(state(2), { state: 'unknown', detail: 'lookup failed' });
assert.deepEqual([state(12).detail, state(13).detail, state(14).detail], ['the number is a pull request', 'lookup returned no issue record', 'lookup failed']);
assert.equal(r.lookups, LOOKUP_BUDGET);
assert.deepEqual([state(20), state(21)], [{ state: 'unknown', detail: 'over the lookup budget' }, { state: 'unknown', detail: 'over the lookup budget' }]);
// The open list is one call; a pull request in it is not an issue, so #2 is looked up.
const calls = f.calls();
assert.equal(calls.length, 1 + LOOKUP_BUDGET);
assert.match(calls[0], /^repos\/mosaicstack\/stack\/issues\?state=open&type=issues&limit=50&page=1$/);
assert.deepEqual(calls.slice(1), [2, 10, 11, 12, 13, 14, 15, 16, 17, 18].map(n => `repos/mosaicstack/stack/issues/${n}`));
assert.equal(r.openListFull, false);
writeFileSync(f.env.OPEN, JSON.stringify(Array.from({ length: 50 }, (_, i) => openIssue(500 + i))));
assert.equal(issueStates('/', [row(1, { issues: [500], closes: [500] })], null, f.tool).openListFull, true);
});
test('a full open list: lookups settle what it leaves out, and only an unsettled issue keeps it undecided', t => {
const f = fakeTool(t);
Object.assign(process.env, f.env);
t.after(() => { for (const k of Object.keys(f.env)) delete process.env[k]; });
writeFileSync(f.env.OPEN, JSON.stringify(Array.from({ length: 50 }, (_, i) => openIssue(500 + i))));
const check = rows => queueChecks(queue(rows), { issues: issueStates('/', rows, null, f.tool), seats: null, now: NOW });
// Every wanted issue resolved: #600 closed by lookup, #501 open on the page for a pending row.
f.issue(600);
const resolved = check([row(1, { state: 'done', issues: [600], closes: [600] }), row(2, { issues: [501], closes: [501] })]);
assert.deepEqual([resolved.issueChecks, resolved.undecided, resolved.violations, resolved.result], [{ run: true, lookups: 1, openListFull: true }, [], [], 'reduced pass']);
assert.ok(formatQueue(resolved).includes('queue issue checks: open list (full page), 1 lookup'));
// An open issue off the page is found by lookup, and its done row fails.
f.issue(601, { state: 'open' });
const offPage = check([row(1, { state: 'done', issues: [601], closes: [601] })]);
assert.deepEqual([ids(offPage.violations), offPage.undecided, offPage.result], [[['issue-open', 1, 601]], [], 'fail']);
// Past the budget an issue stays unknown, and the full page stays undecided.
const many = Array.from({ length: LOOKUP_BUDGET + 1 }, (_, i) => 700 + i);
for (const n of many) f.issue(n);
const budget = check(many.map(n => row(n, { state: 'done', issues: [n], closes: [n] })));
assert.deepEqual(ids(budget.undecided), [['issue-unknown', 710, 710], ['open-list-full', null, null]]);
assert.match(budget.undecided[0].message, /is unknown \(over the lookup budget\)$/);
assert.equal(budget.result, 'incomplete');
});
test('the open list refuses on a failed call or a bad record, and never echoes the helper', t => {
const f = fakeTool(t);
Object.assign(process.env, f.env);
t.after(() => { for (const k of [...Object.keys(f.env), 'OPEN_FAIL', 'API_FAIL']) delete process.env[k]; });
const rows = [row(1, { issues: [1], closes: [1] })];
const refuses = (pattern) => assert.throws(() => issueStates('/', rows, null, f.tool), e => e.exitCode === 2 && pattern.test(e.message) && !/secret/.test(e.message));
process.env.API_FAIL = '3'; refuses(/credentials missing, unreadable, or invalid/); delete process.env.API_FAIL;
process.env.OPEN_FAIL = '1'; refuses(/credential or Gitea request failure/); delete process.env.OPEN_FAIL;
for (const bad of ['nope', '{}', '[{"number":0,"state":"open"}]', '[{"number":1,"state":"merged"}]', '[{"number":1,"state":"closed"}]']) {
writeFileSync(f.env.OPEN, bad);
refuses(/Open issues unavailable/);
}
});
test('a helper call past the deadline is killed with its child, and the call reports it', t => {
const dir = mkdtempSync(path.join(os.tmpdir(), 'ledger-slow-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
// The helper starts a child, as gitea-api.sh starts curl, and both hang.
const tool = path.join(dir, 'gitea-api.sh');
writeFileSync(tool, `#!/usr/bin/env node
const { spawn } = require('child_process');
const child = spawn('sleep', ['30'], { stdio: 'ignore' });
require('fs').appendFileSync('${dir}/pids', child.pid + '\\n');
setTimeout(() => {}, 30000);
`);
chmodSync(tool, 0o755);
const started = Date.now();
assert.throws(() => readOpenIssues('/', tool, 1), e => e.exitCode === 2 && /^Open issues unavailable: no answer within 1 s; use --no-issues/.test(e.message));
assert.deepEqual(lookupIssue('/', 5, tool, 1), { state: 'unknown', detail: 'lookup failed' });
assert.ok(Date.now() - started < 10000, 'each call returns at its deadline');
const pids = readFileSync(path.join(dir, 'pids'), 'utf8').split('\n').filter(Boolean).map(Number);
assert.equal(pids.length, 2);
// Give init a moment to reap the killed children.
const until = Date.now() + 3000;
while (pids.some(pidAlive) && Date.now() < until) spawnSync('sleep', ['0.1']);
assert.deepEqual(pids.filter(pidAlive), [], 'no child outlives the call');
assert.throws(() => readOpenIssues('/', path.join(dir, 'absent'), 1), /Open issues unavailable: gitea-api\.sh unavailable;/);
});
test('readQueue loads queue.json through the queue validator and refuses anything else', t => {
const repo = scratchRepo(t);
genesisCommitted(repo);
const q = readQueue(repo.root);
assert.deepEqual([q.revision, q.canonicalRoot, q.rows.map(r => r.id)], [0, repo.root, [1, 6, 8, 9, 11]]);
assert.match(q.genesisAt, /^\d{4}-\d{2}-\d{2}T/);
const text = readFileSync(repo.queuePath, 'utf8');
writeFileSync(repo.queuePath, text.replace('"state": "briefed"', '"state": "done"'));
assert.throws(() => readQueue(repo.root), /Queue unavailable: .*hand edit/);
rmSync(repo.queuePath);
assert.throws(() => readQueue(repo.root), /missing or not a regular file; use --no-queue/);
writeFileSync(path.join(repo.base, 'elsewhere.json'), text);
symlinkSync(path.join(repo.base, 'elsewhere.json'), repo.queuePath);
assert.throws(() => readQueue(repo.root), /missing or not a regular file/);
});
test('protected changes list every in-range entry that changes a required or parked row', t => {
const repo = scratchRepo(t);
genesisCommitted(repo);
// Row 9 is required, row 6 is neither required nor parked.
for (const [id, op] of [[9, 'note-row9-0001'], [6, 'note-row6-0001']]) {
const r = queueCli(repo, ['note', String(id), 'text', '--op', op], { by: 'darkwing' });
assert.equal(r.code, 0, r.err);
}
// Unparking row 8 leaves it neither parked nor required: the row before
// the entry makes it protected.
const unpark = queueCli(repo, ['move', '8', 'queued', '--op', 'move-row8-0001'], { by: 'jason' });
assert.equal(unpark.code, 0, unpark.err);
const { log } = readQueue(repo.root);
const all = protectedChanges(log, { start: 0, end: Infinity });
assert.deepEqual(all.map(c => [c.rev, c.verb, c.by, c.rows]), [[0, 'genesis', 'sage', [8, 9]], [1, 'note', 'darkwing', [9]], [3, 'move', 'jason', [8]]]);
assert.equal(all[1].op, 'note-row9-0001');
// The range includes its start and excludes its end. A range after
// genesis replays up to its first entry and starts there.
assert.ok(Date.parse(log[0].at) < Date.parse(log[1].at) && Date.parse(log[1].at) < Date.parse(log[2].at));
assert.deepEqual(protectedChanges(log, { start: Date.parse(log[1].at), end: Infinity }).map(c => c.rev), [1, 3]);
assert.deepEqual(protectedChanges(log, { start: 0, end: Date.parse(log[1].at) }).map(c => c.rev), [0]);
assert.deepEqual(protectedChanges(log, { start: Date.parse(log[1].at), end: Date.parse(log[2].at) }).map(c => c.rev), [1]);
assert.deepEqual(protectedChanges(log, { start: 0, end: Date.parse(log[0].at) }), []);
const q = queueChecks(readQueue(repo.root), { issues: null, seats: null, exempt: new Set(['darkwing']), now: NOW, range: { start: 0, end: Infinity } });
const text = formatQueue(q);
assert.ok(text.includes('protected change rev 1 note by darkwing at ' + log[1].at + ': row 9'));
assert.ok(text.includes('protected changes in range: 3 (not checks; confirm the actors)'));
assert.equal(q.result, 'incomplete', 'the list is not a finding');
});
// A scratch repository with a committed queue, the ledger and the seat code,
// a config whose dataRoot holds the registrations, and a fake helper.
const MAP = [
{ id: 1, piece: 'Closed and done', owner: 'darkwing', issues: [101], closes: [101], state: 'done', required: false, requiredSince: null, brief: null },
{ id: 2, piece: 'Done but open', owner: 'darkwing', issues: [102], closes: [102], state: 'done', required: false, requiredSince: null, brief: null },
{ id: 3, piece: 'Active and old', owner: 'dewey', issues: [103], closes: [103], state: 'in-progress', required: true, requiredSince: '2020-01-01', brief: { path: 'docs/plans/brief-a.md', anchor: 'Row six' } },
{ id: 4, piece: 'Active and registered', owner: 'rocko', issues: [103], closes: [], state: 'in-progress', required: false, requiredSince: null, brief: { path: 'docs/plans/brief-a.md', anchor: 'Row six' } },
].map(r => {
const full = { previousState: null, gate: 'g', gateOwner: 'jason', after: [], reviewers: [], note: null, blockedReason: null, createdAt: '2026-09-13', ...r };
// The map wants its keys in the queue's order.
return Object.fromEntries(Object.keys(MAP_ROWS[0]).map(k => [k, full[k]]));
});
function cliFixture(t, rows = MAP) {
const repo = scratchRepo(t, { map: mapText(rows, { retired: [], highWater: Math.max(...rows.map(r => r.id)) }) });
genesisCommitted(repo);
for (const pkg of ['ledger', 'seat']) cpSync(path.join(PACKAGES, pkg, 'src'), path.join(repo.root, 'packages', pkg, 'src'), { recursive: true });
const dataRoot = path.join(repo.base, 'data');
const config = path.join(repo.base, 'config.json');
writeFileSync(config, JSON.stringify({ dataRoot }));
const f = fakeTool(t);
f.issue(101);
writeFileSync(f.env.OPEN, JSON.stringify([openIssue(102), openIssue(103)]));
writeFileSync(f.env.ISSUES, '[]');
const seats = path.join(dataRoot, 'seats');
mkdirSync(seats, { recursive: true });
writeRegistration(seats, { version: 1, seat: 'rocko', project: 'repo', task: '', workspace: repo.root, tmux: null, harness: 'pi', startedAt: '2026-09-27T00:00:00.000Z',
pid: process.pid, sessionsDir: path.join(repo.root, '.pi/state/rocko/sessions'), seatDir: path.join(repo.root, 'agents/rocko'), launchScript: path.join(repo.root, 'agents/rocko/launch.sh'), layout: 'repo', updatedAt: null });
const run = (args = [], env = {}, { until = '2026-09-26' } = {}) => {
writeFileSync(f.env.CALLS, '');
const r = spawnSync(process.execPath, [path.join(repo.root, 'packages/ledger/src/cli.mjs'), '--since', '2026-09-20', '--until', until, '--no-t3', ...args],
{ cwd: repo.root, encoding: 'utf8', env: { ...repo.env, PATH: `${f.dir}:${process.env.PATH}`, MOSAIC_CONFIG: config, ...f.env, ...env } });
return { ...r, calls: f.calls() };
};
return { repo, f, run, config };
}
test('the CLI prints the queue section above the weekly table and under a queue key in --json', t => {
const { run } = cliFixture(t);
const text = run(['--unsupported-runtime', 'dewey']);
assert.equal(text.status, 0, text.stderr);
const lines = text.stdout.split('\n');
assert.match(lines[0], /^Queue checks: queue\.json revision 0, as of \d{4}-/);
assert.ok(lines.indexOf('queue: 2 violations; result fail') < lines.findIndex(l => l.startsWith('Ledger 2026-09-20')));
assert.ok(lines.includes('violation issue-open row 2 #102: row 2 is done and every row that closes #102 is done, but #102 is open'));
assert.ok(lines.some(l => /^violation age row 3: row 3 "Active and old" is required and not done, \d+ days since 2020-01-01$/.test(l)));
assert.ok(lines.includes('owner dewey (row 3): exempt, declared with --unsupported-runtime'));
assert.ok(lines.includes('owner rocko (row 4): pid-present, pid present (identity not verified)'));
assert.ok(lines.includes('liveness: 1 pid-present (unverified), 1 exempt, 0 pid-unknown, 0 missing, 0 invalid, 0 pid-gone'));
assert.ok(lines.includes('queue issue checks: open list, 1 lookup'));
// Metric page, open list, one lookup for #101.
assert.deepEqual(text.calls.map(c => c.replace(/\?.*/, '?')), ['repos/mosaicstack/stack/issues?', 'repos/mosaicstack/stack/issues?', 'repos/mosaicstack/stack/issues/101']);
const json = JSON.parse(run(['--json', '--unsupported-runtime', 'dewey']).stdout);
assert.deepEqual([json.queue.result, json.queue.violations.map(v => [v.check, v.row, v.issue])], ['fail', [['issue-open', 2, 102], ['age', 3, null]]]);
// Genesis is dated today, outside the requested week.
assert.deepEqual(json.queue.protectedChanges, []);
const week = JSON.parse(run(['--json', '--no-issues'], {}, { until: new Date().toISOString().slice(0, 10) }).stdout).queue;
assert.deepEqual(week.protectedChanges.map(c => [c.rev, c.verb, c.rows]), [[0, 'genesis', [3]]]);
// Without the declaration, dewey has no registration.
const missing = JSON.parse(run(['--json']).stdout).queue;
assert.deepEqual(missing.violations.map(v => v.check), ['issue-open', 'owner-missing', 'age']);
});
test('a queue with nothing wrong prints 0 violations and a reduced pass, never a full pass', t => {
const { run } = cliFixture(t, [MAP[0], MAP[3]]);
const r = run();
assert.equal(r.status, 0, r.stderr);
const lines = r.stdout.split('\n');
assert.ok(lines.includes('queue: 0 violations; result reduced pass'));
assert.ok(lines.includes('liveness: 1 pid-present (unverified), 0 exempt, 0 pid-unknown, 0 missing, 0 invalid, 0 pid-gone'));
assert.ok(!lines.some(l => /^(violation|undecided|disposition) /.test(l)));
assert.equal(r.calls.length, 3, 'metric page, open list, one lookup');
});
test('--no-issues makes no call and leaves the issue checks undecided; --no-queue skips the section', t => {
const { run } = cliFixture(t);
const none = run(['--no-issues', '--json']);
assert.equal(none.status, 0, none.stderr);
assert.deepEqual(none.calls, []);
assert.deepEqual(JSON.parse(none.stdout).queue.issueChecks, { run: false });
const skipped = run(['--no-queue']);
assert.equal(skipped.status, 0, skipped.stderr);
assert.match(skipped.stdout, /^Queue: not checked \(--no-queue\)\nLedger 2026-09-20/);
assert.equal(skipped.calls.length, 1, 'the metric call only');
assert.deepEqual(JSON.parse(run(['--no-queue', '--json']).stdout).queue, { checked: false });
});
test('the CLI refuses a bad queue before any call, and a failed open list with exit 2', t => {
const { repo, run } = cliFixture(t);
const refusedOpen = run([], { OPEN_FAIL: '1' });
assert.deepEqual([refusedOpen.status, refusedOpen.stdout], [2, '']);
assert.match(refusedOpen.stderr, /^Open issues unavailable: credential or Gitea request failure; use --no-issues/);
const text = readFileSync(repo.queuePath, 'utf8');
writeFileSync(repo.queuePath, text.replace('"Done but open"', '"Done, hand edited"'));
const edited = run();
assert.deepEqual([edited.status, edited.stdout, edited.calls], [1, '', []]);
assert.match(edited.stderr, /^Queue unavailable: /);
rmSync(repo.queuePath);
const gone = run();
assert.deepEqual([gone.status, gone.calls], [1, []]);
assert.match(gone.stderr, /use --no-queue to skip the queue section/);
});
test('--unsupported-runtime repeats once per seat and takes a seat name', t => {
const { run } = cliFixture(t);
const two = JSON.parse(run(['--json', '--no-issues', '--unsupported-runtime', 'dewey', '--unsupported-runtime', 'rocko']).stdout).queue;
assert.deepEqual([two.liveness.exempt, two.liveness.counts.exempt], [['dewey', 'rocko'], 2]);
for (const [args, message] of [
[['--unsupported-runtime', 'dewey', '--unsupported-runtime', 'dewey'], /^Duplicate --unsupported-runtime dewey\n$/],
[['--unsupported-runtime'], /needs a seat name/],
[['--unsupported-runtime', '--json'], /needs a seat name/],
[['--unsupported-runtime', 'Dewey'], /needs a seat name/],
[['--no-queue', '--unsupported-runtime', 'dewey'], /cannot be combined/],
]) {
const r = run(args);
assert.equal(r.status, 1, args.join(' '));
assert.match(r.stderr, message);
assert.deepEqual([r.stdout, r.calls], ['', []]);
}
});
test('an unreadable config makes every owner invalid instead of passing them', t => {
const { run } = cliFixture(t);
const r = JSON.parse(run(['--json', '--no-issues'], { MOSAIC_CONFIG: '/nonexistent/config.json' }).stdout).queue;
assert.deepEqual(r.liveness.seats.map(s => [s.seat, s.class]), [['dewey', 'invalid'], ['rocko', 'invalid']]);
assert.equal(r.result, 'fail');
});