// 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'); });