Files
stack/packages/ledger/tests/ledger.test.mjs
T
jason.woltjeandClaude Opus 5.5 a5b8a99a18 ledger: skip T3 reasoning summaries and read issue pages up to a ceiling (row 113, #1607)
T3 messages with role reasoning and an id starting "reasoning:" are now
skipped before any other check and counted as reasoningMessages. Any other
unknown role, including reasoning with another id prefix, still refuses.
Issues updated since the start date are read in pages of 50 until a short
page, at most 10 pages. A full tenth page, a duplicate issue number across
pages, a page over 50 rows or any failed page refuses with exit 2, so
partial totals are never reported. The report prints how many pages it
read. The README describes the skip, the page loop and its refusals.

Candidate manifest 4eed1d6405a505e88cde5062c61f768639d899b8f301019b1403d68fb15d9d36
(Darkwing round 1, packet d6ea56831), BASE cc48b124f. Approved by Filbert
(comment 28407). Sage gate rerun at 644ef5310 plus the candidate passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:35:33 -05:00

518 lines
36 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, cpSync, symlinkSync, realpathSync } from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync, spawnSync } from 'node:child_process';
import { dateRange, messageKind, issueNumbers, totalsLine, summarize } from '../src/ledger.mjs';
import { readT3 } from '../src/t3.mjs';
const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src');
const range = dateRange('2026-09-06', '2026-09-12');
// T3 fixture schema: the live tables, cut to the columns the reader uses plus
// one it doesn't. `text` allows NULL so a non-text row can be tested.
const T3_SCHEMA = `
create table projection_projects (project_id text primary key, title text not null, workspace_root text not null, deleted_at text);
create table projection_threads (thread_id text primary key, project_id text not null, title text not null, archived_at text, deleted_at text);
create table projection_thread_messages (message_id text primary key, thread_id text not null, role text not null, text, created_at text not null);
create table orchestration_events (sequence integer primary key autoincrement, stream_id text not null, event_type text not null, payload_json text not null, metadata_json text not null);`;
// Writes a T3 database in WAL mode. Threads default to project p1, which is
// the fixture root. Returns the open writer when keepOpen is set.
function t3db(file, { root, projects, threads = [], messages = [], after = [], keepOpen = false }) {
mkdirSync(path.dirname(file), { recursive: true });
for (const old of [file, `${file}-wal`, `${file}-shm`]) rmSync(old, { force: true });
const db = new DatabaseSync(file);
db.exec('pragma journal_mode=wal'); db.exec(T3_SCHEMA);
for (const [id, workspace, deleted = null] of projects ?? [['p1', root]]) {
db.prepare('insert into projection_projects values (?, ?, ?, ?)').run(id, 'project', workspace, deleted);
}
for (const t of threads) {
db.prepare('insert into projection_threads values (?, ?, ?, ?, ?)').run(t.id, t.project ?? 'p1', t.title, t.archived ?? null, t.deleted ?? null);
}
for (const m of messages) addMessage(db, m);
for (const sql of after) db.exec(sql);
if (keepOpen) return db;
db.close();
}
let messageId = 0;
function addMessage(db, { thread, text, role = 'user', at = '2026-09-08T12:00:00Z', origin = 'app', id = `m${++messageId}` }) {
db.prepare('insert into projection_thread_messages values (?, ?, ?, ?, ?)').run(id, thread, role, text, at);
if (origin !== 'none') db.prepare('insert into orchestration_events (stream_id, event_type, payload_json, metadata_json) values (?, ?, ?, ?)')
.run(thread, 'thread.message-sent', JSON.stringify({ messageId: id, threadId: thread, role, text }), JSON.stringify({ origin: origin === 'app' ? { appVersion: '0.0.0' } : {} }));
}
const fixtureIssues = [
{ number: 1, title: 'First issue', created_at: '2026-09-06T00:00:00Z', closed_at: '2026-09-07T12:00:00Z' },
{ number: 2, title: 'Second issue', created_at: '2026-09-06T00:00:00Z', closed_at: null },
];
function fixture(t) {
const root = mkdtempSync(path.join(os.tmpdir(), 'ledger-test-'));
// No test opens the real ~/.t3: every CLI run gets this HOME, with an empty
// T3 database at the default path. The one in-process readT3 call passes an
// explicit fixture path. The CLI's root is a realpath.
const home = mkdtempSync(path.join(os.tmpdir(), 'ledger-home-'));
t.after(() => { rmSync(root, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true }); });
const defaultDb = path.join(home, '.t3/userdata/state.sqlite');
t3db(defaultDb, { root: realpathSync(root) });
const put = (name, data) => { const p = path.join(root, name); mkdirSync(path.dirname(p), { recursive: true }); writeFileSync(p, data); return p; };
const git = (args, date = '2026-09-07T00:00:00Z') => execFileSync('git', args, { cwd: root, env: { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' }, stdio: 'pipe' });
git(['init', '-b', 'refactor']); git(['config', 'user.email', '[email protected]']); git(['config', 'user.name', 'Fixture']);
const commit = (subject, date, body) => git(['-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', subject, ...(body ? ['-m', body] : [])], date);
commit('first #1'); commit('first #2'); commit('follow-up #2'); commit('untagged', undefined, 'body only #3');
mkdirSync(path.join(root, 'agents/alice'), { recursive: true });
mkdirSync(path.join(root, 'agents/bob'), { recursive: true });
cpSync(source, path.join(root, 'packages/ledger/src'), { recursive: true });
// The queue section imports the queue validator and the seat registry.
for (const pkg of ['queue', 'seat']) cpSync(path.join(source, '../..', pkg, 'src'), path.join(root, 'packages', pkg, 'src'), { recursive: true });
// The fake serves ISSUES for every page, or page N of the ISSUE_PAGES array
// (an empty page past its end). API_FAIL fails every call, or only FAIL_PAGE.
const api = put('bin/gitea-api.sh', `#!/usr/bin/env node
const fs = require('fs'), args = process.argv.slice(2), page = Number(new URL(args[1], 'http://x/').searchParams.get('page'));
fs.appendFileSync(process.env.CALLS, JSON.stringify(args) + '\\n');
if (process.env.API_FAIL && (!process.env.FAIL_PAGE || Number(process.env.FAIL_PAGE) === page)) { console.error('secret must not escape'); process.exit(Number(process.env.API_FAIL)); }
if (process.env.ISSUE_PAGES) process.stdout.write(JSON.stringify(JSON.parse(fs.readFileSync(process.env.ISSUE_PAGES, 'utf8'))[page - 1] ?? []));
else process.stdout.write(fs.readFileSync(process.env.ISSUES, 'utf8'));
`);
execFileSync('chmod', ['+x', api]);
put('issues.json', JSON.stringify(fixtureIssues)); put('calls.jsonl', '');
const entry = (text, timestamp = '2026-09-08T12:00:00Z') => ({ type: 'message', timestamp, message: { role: 'user', content: [{ type: 'text', text }] } });
const logs = [entry('[host:control-board -> host:alice] do #1'), entry('[host:bob -> host:alice] review #2'), entry('build #2'), entry('old #1', '2026-09-05T23:59:59Z'), { type: 'message', timestamp: '2026-09-08T00:00:00Z', message: { role: 'assistant', content: 'not a user #1' } }];
put('.pi/state/alice/sessions/one.jsonl', logs.map(x => JSON.stringify(x)).join('\n') + '\n');
// These tests cover the weekly table; queue-checks.test.mjs covers the queue section.
const run = (args = [], env = {}) => spawnSync(process.execPath, [path.join(root, 'packages/ledger/src/cli.mjs'), '--since', '2026-09-06', '--until', '2026-09-12', '--no-queue', ...args], { cwd: root, encoding: 'utf8', env: { ...process.env, HOME: home, PATH: `${path.join(root, 'bin')}:${process.env.PATH}`, ISSUES: path.join(root, 'issues.json'), CALLS: path.join(root, 'calls.jsonl'), ...env } });
return { root, real: realpathSync(root), home, defaultDb, put, commit, run, entry, logs };
}
test('fixture git subjects only, follow-ups and three session kinds', t => {
const f = fixture(t), result = f.run(['--json']);
assert.equal(result.status, 0, result.stderr);
const r = JSON.parse(result.stdout);
assert.deepEqual(r.issues.map(x => [x.issue, x.commits, x.followUps, x.hoursOpen, x.seats]), [[1, 1, 0, 36, ['alice']], [2, 2, 1, 'open', ['alice']]]);
assert.deepEqual(r.seats, [{ seat: 'alice', board: 1, agent: 1, human: 1 }]);
assert.deepEqual(r.totals, { issuesClosed: 1, medianHoursOpen: 36, commits: 3, followUpsPerIssue: 0.5, humanMessagesPerClosedIssue: 1 });
const calls = readFileSync(path.join(f.root, 'calls.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
assert.equal(calls.length, 1); assert.equal(calls[0][0], 'GET');
assert.match(calls[0][1], /^repos\/mosaicstack\/stack\/issues\?state=all&type=issues&since=2026-09-06T00%3A00%3A00.000Z&limit=50&page=1$/);
});
test('text and JSON carry same numbers, open and truncated title', t => {
const f = fixture(t); f.put('issues.json', JSON.stringify([{ ...fixtureIssues[0], title: 'x'.repeat(100) }, fixtureIssues[1]]));
const text = f.run(), json = f.run(['--json']);
assert.equal(text.status, 0, text.stderr); const r = JSON.parse(json.stdout);
assert.ok(text.stdout.includes(totalsLine(r.totals)));
assert.match(text.stdout, /#1 \| x{48} \| .* \| 36.0 \| 1 \| 0 \| alice/);
assert.match(text.stdout, /#2 \| Second issue \| .* \| open \| 2 \| 1 \| alice/);
assert.match(text.stdout, /alice \| 1 \| 1 \| 1/);
});
test('missing credentials exit 2, no-issues never calls API and shows unknown', t => {
const f = fixture(t); const bad = f.run([], { API_FAIL: '3' });
assert.equal(bad.status, 2); assert.match(bad.stderr, /credentials missing, unreadable, or invalid/); assert.doesNotMatch(bad.stderr, /secret must/); assert.equal(bad.stdout, '');
f.put('calls.jsonl', ''); const skip = f.run(['--no-issues'], { API_FAIL: '3' });
assert.equal(skip.status, 0, skip.stderr); assert.match(skip.stdout, /#1 \| unknown \| unknown \| unknown/);
assert.match(skip.stdout, /issues closed unknown/); assert.equal(readFileSync(path.join(f.root, 'calls.jsonl'), 'utf8'), '');
});
test('empty range gives no rows and zero totals', t => {
const f = fixture(t);
f.put('issues.json', '[]');
const result = spawnSync(process.execPath, [path.join(f.root, 'packages/ledger/src/cli.mjs'), '--since', '2027-01-01', '--until', '2027-01-02', '--json', '--no-queue'], { encoding: 'utf8', env: { ...process.env, HOME: f.home, PATH: `${f.root}/bin:${process.env.PATH}`, ISSUES: `${f.root}/issues.json`, CALLS: `${f.root}/calls.jsonl` } });
assert.equal(result.status, 0, result.stderr); const r = JSON.parse(result.stdout);
assert.deepEqual(r.issues, []); assert.deepEqual(r.seats, []); assert.ok(Object.values(r.totals).every(n => n === 0));
});
test('inclusive UTC dates, first-line preamble only, role and seat boundaries', t => {
const f = fixture(t);
f.put('.pi/state/alice/sessions/one.jsonl', [f.entry('start #1', '2026-09-06T00:00:00Z'), f.entry('end #2', '2026-09-12T23:59:59.999Z'), f.entry('outside', '2026-09-13T00:00:00Z'), f.entry('human\n[host:control-board -> host:alice] quoted')].map(JSON.stringify).join('\n'));
f.put('.pi/state/not-a-seat/sessions/one.jsonl', JSON.stringify(f.entry('ignored')));
f.commit('at end #2', '2026-09-12T23:59:59Z'); f.commit('outside #2', '2026-09-13T00:00:00Z');
const result = f.run(['--json']); assert.equal(result.status, 0, result.stderr); const r = JSON.parse(result.stdout);
assert.equal(r.seats[0].human, 3); assert.equal(r.totals.commits, 4);
});
test('close-only issue included, even median, missing metadata stays unknown', t => {
const f = fixture(t);
f.put('issues.json', JSON.stringify([fixtureIssues[0], { number: 3, title: 'close only', created_at: '2026-09-06T00:00:00Z', closed_at: '2026-09-08T12:00:00Z' }]));
const r = JSON.parse(f.run(['--json']).stdout);
assert.equal(r.issues[1].hoursOpen, 'unknown'); assert.equal(r.issues[2].commits, 0); assert.equal(r.totals.medianHoursOpen, 48); assert.equal(r.totals.issuesClosed, 2);
});
test('unique commits but per-issue links count multiple tags once each', t => {
const f = fixture(t); f.commit('both #1 #2 #2'); const r = JSON.parse(f.run(['--json']).stdout);
assert.equal(r.totals.commits, 4); assert.deepEqual(r.issues.map(x => x.commits), [2, 3]);
});
// Issue pages. Each issue closes in range, so issuesClosed counts every one read.
const closedIssue = number => ({ number, title: `Issue ${number}`, created_at: '2026-09-06T00:00:00Z', closed_at: '2026-09-07T00:00:00Z' });
const issuePages = sizes => { let next = 0; return sizes.map(n => Array.from({ length: n }, () => closedIssue(++next))); };
function paged(t, pages) {
const f = fixture(t), file = f.put('pages.json', JSON.stringify(pages));
const calls = () => readFileSync(path.join(f.root, 'calls.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l)[1]);
return { ...f, run: (args = [], env = {}) => f.run(args, { ISSUE_PAGES: file, ...env }), calls };
}
test('issue pages of 50, 50 and 7 read 107 issues in 3 pages', t => {
const f = paged(t, issuePages([50, 50, 7]));
const result = f.run(['--json']); assert.equal(result.status, 0, result.stderr);
const r = JSON.parse(result.stdout);
assert.equal(r.totals.issuesClosed, 107); assert.deepEqual(r.issueSource, { read: true, pages: 3 });
const calls = f.calls(); assert.equal(calls.length, 3);
calls.forEach((call, i) => assert.match(call, new RegExp(`^repos/mosaicstack/stack/issues\\?state=all&type=issues&since=2026-09-06T00%3A00%3A00.000Z&limit=50&page=${i + 1}$`)));
const text = f.run(); assert.equal(text.status, 0, text.stderr);
assert.match(text.stdout, /^Issues: 3 pages read from Gitea$/m); assert.match(text.stdout, /issues closed 107 /);
});
test('one short page reads 1 page; --no-issues reads none and says so', t => {
const f = fixture(t), one = f.run();
assert.equal(one.status, 0, one.stderr); assert.match(one.stdout, /^Issues: 1 page read from Gitea$/m);
assert.deepEqual(JSON.parse(f.run(['--no-issues', '--json']).stdout).issueSource, { read: false });
assert.match(f.run(['--no-issues']).stdout, /^Issues: not read \(--no-issues\)$/m);
});
test('a short page 10 is read; a full page 10 refuses at the ceiling', t => {
const last = paged(t, issuePages([...Array(9).fill(50), 1]));
const ok = last.run(['--json']); assert.equal(ok.status, 0, ok.stderr);
assert.deepEqual(JSON.parse(ok.stdout).issueSource, { read: true, pages: 10 }); assert.equal(JSON.parse(ok.stdout).totals.issuesClosed, 451);
const f = paged(t, issuePages(Array(11).fill(50)));
const result = f.run(); assert.equal(result.status, 2); assert.equal(result.stdout, '');
assert.match(result.stderr, /page 10 is full, so the 10-page ceiling was reached/);
assert.equal(f.calls().length, 10);
});
test('an issue number on two pages refuses', t => {
const pages = issuePages([50, 7]); pages[1][3] = closedIssue(50);
const f = paged(t, pages), result = f.run();
assert.equal(result.status, 2); assert.equal(result.stdout, ''); assert.match(result.stderr, /duplicate issue number on page 2/);
});
test('a failing page 2 refuses the whole read without its stderr', t => {
const f = paged(t, issuePages([50, 50, 7])), result = f.run([], { API_FAIL: '1', FAIL_PAGE: '2' });
assert.equal(result.status, 2); assert.equal(result.stdout, '');
assert.match(result.stderr, /credential or Gitea request failure on page 2/); assert.doesNotMatch(result.stderr, /secret must/);
assert.equal(f.calls().length, 2);
});
for (const [name, bad] of [['a malformed record', { number: 'x' }], ['more than 50 rows', null]]) test(`page 2 with ${name} refuses`, t => {
const pages = issuePages([50, 7]);
if (bad) pages[1][0] = bad; else pages[1] = issuePages([101])[0].slice(50);
const result = paged(t, pages).run(); assert.equal(result.status, 2); assert.equal(result.stdout, '');
assert.match(result.stderr, /invalid Gitea issue records on page 2/);
});
for (const payload of ['not JSON', '{}', '[{"number":1}]']) test(`bad API payload ${payload} refuses`, t => {
const f = fixture(t); f.put('issues.json', payload); const r = f.run(); assert.equal(r.status, 2); assert.equal(r.stdout, '');
});
test('partial or malformed session log refuses with location, not content', t => {
const f = fixture(t); f.put('.pi/state/alice/sessions/bad.jsonl', '{sensitive'); const r = f.run();
assert.equal(r.status, 1); assert.match(r.stderr, /Malformed session JSON: alice\/bad.jsonl:1/); assert.doesNotMatch(r.stderr, /sensitive/);
});
test('a U+2028 or U+2029 inside a session string is one line, not a malformed record', t => {
const f = fixture(t);
f.put('.pi/state/bob/sessions/sep.jsonl', [f.entry('Jason: one\u2028two\u2029three #2'), f.entry('[h:alice -> h:bob] ok')].map(x => JSON.stringify(x)).join('\r\n') + '\r\n');
const written = readFileSync(path.join(f.root, '.pi/state/bob/sessions/sep.jsonl'), 'utf8');
assert.ok(written.includes('\u2028') && written.includes('\u2029'));
const r = f.run(['--json']); assert.equal(r.status, 0, r.stderr);
assert.deepEqual(JSON.parse(r.stdout).seats[1], { seat: 'bob', board: 0, agent: 1, human: 1 });
});
test('no sessions is an empty table; symlink source refuses', t => {
const f = fixture(t); rmSync(path.join(f.root, '.pi'), { recursive: true });
assert.deepEqual(JSON.parse(f.run(['--json']).stdout).seats, []);
symlinkSync(path.join(f.root, 'agents'), path.join(f.root, '.pi'));
const r = f.run(); assert.equal(r.status, 1); assert.match(r.stderr, /Cannot read ledger directory/);
});
test('reads only refactor even when another branch is checked out', t => {
const f = fixture(t); execFileSync('git', ['checkout', '-b', 'other'], { cwd: f.root, stdio: 'pipe' }); f.commit('other #1');
assert.equal(JSON.parse(f.run(['--json']).stdout).totals.commits, 3);
});
test('invalid dates, reverse dates and duplicate options refuse', t => {
assert.throws(() => dateRange('2026-02-30')); assert.throws(() => dateRange('2026-09-12', '2026-09-06'));
const f = fixture(t); assert.equal(f.run(['--since', '2026-09-01']).status, 1);
});
test('T3 agent assignments do not count as human in Table 2', t => {
const f = fixture(t);
f.put('.pi/state/bob/sessions/t3.jsonl', [
f.entry('[from: sage (1ef1e4f8) -> to: bob (9cb9731e) class=actionable]\nassign #1'),
f.entry('[from: sage (1ef1e4f8) -> to: bob (9cb9731e)]\nfollow-up #1'),
f.entry('Jason: go ahead'),
].map(x => JSON.stringify(x)).join('\n') + '\n');
const result = f.run(['--json']);
assert.equal(result.status, 0, result.stderr);
const r = JSON.parse(result.stdout);
assert.deepEqual(r.seats, [{ seat: 'alice', board: 1, agent: 1, human: 1 }, { seat: 'bob', board: 0, agent: 2, human: 1 }]);
assert.equal(r.totals.humanMessagesPerClosedIssue, 2);
});
test('preamble parsing and issue number boundaries', () => {
assert.equal(messageKind('[h:control-board -> h:seat] hi'), 'board');
assert.equal(messageKind('[h:seat -> h:seat class=actionable] hi'), 'agent');
assert.equal(messageKind('human\n[h:control-board -> h:seat] hi'), 'human');
assert.equal(messageKind(' [h:seat -> h:seat] quoted'), 'human');
assert.deepEqual(issueNumbers('fix #1 #2 #2 abc#3 #0 #4x'), [1, 2]);
});
test('T3 header: agent, or board from control-board; anything short of the full header is human', () => {
const sage = 'sage (1ef1e4f8-3ead-4208-beca-38f9f1add079)', filbert = 'filbert (9cb9731e-a10f-4c8f-a212-c4fa1f5f4731)';
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}]\nbuild #1506`), 'agent');
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=actionable]\nbuild`), 'agent');
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}] same line`), 'agent');
assert.equal(messageKind(`[from: darkwing (thread-id: unknown) -> to: reviewer (new-thread)]\nreview`), 'agent');
assert.equal(messageKind(`[from: control-board (b) -> to: ${filbert}]\nhi`), 'board');
assert.equal(messageKind(`Jason here\n[from: ${sage} -> to: ${filbert}]\nquoted`), 'human');
assert.equal(messageKind(` [from: ${sage} -> to: ${filbert}]`), 'human');
assert.equal(messageKind(`[from: sage -> to: filbert]\nno thread ids`), 'human');
// Classes match in either case (Gate F). HEAD before the fix called these human.
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=Actionable]`), 'agent');
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=REVIEW-REQUEST]\nreview`), 'agent');
assert.equal(messageKind('[h:sage -> h:bob class=DECISION] go'), 'agent');
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=review_request]`), 'human');
assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}]trailing`), 'human');
assert.equal(messageKind(`[From: ${sage} -> to: ${filbert}]`), 'human');
});
test('no closed issues with human messages means undefined ratio, not invented zero', () => {
const r = summarize(range, [], [], { rows: [{ seat: 'a', human: 1, board: 0, agent: 0 }], mentions: new Map() });
assert.equal(r.totals.humanMessagesPerClosedIssue, 'unknown');
});
// T3 thread source (Gate F, docs/plans/2026-09-26_ledger-t3-source.md).
const T1 = 't-alice', T2 = 't-bob', T3 = 't-sagebrush', T4 = 't-researcher', T5 = 't-discord';
const header = (from, to, toId, cls = '') => `[from: ${from} (x1) -> to: ${to} (${toId})${cls}]`;
function t3Fixture(t) {
const f = fixture(t);
for (const seat of ['sage', 'researcher']) mkdirSync(path.join(f.root, 'agents', seat), { recursive: true });
const db = path.join(f.home, 'fixture/t3.sqlite');
const threads = [
{ id: T1, title: 'Alice' }, { id: T2, title: 'Bob in Claude', archived: '2026-09-09T00:00:00Z' },
{ id: T3, title: 'Sagebrush' }, { id: T4, title: 'Researcher' }, { id: T5, title: 'Discord Bot' },
{ id: 'import:claudeAgent:1', title: 'alice' }, { id: 't-deleted', title: 'Alice', deleted: '2026-09-09T00:00:00Z' },
{ id: 't-other', project: 'p2', title: 'Alice' },
];
const messages = [
{ thread: T1, text: 'Jason: go #1' },
{ thread: T1, text: `${header('sage', 'alice', T1, ' class=REVIEW-REQUEST')}\nreview #2`, origin: 'api' },
{ thread: T1, text: '[h:sage -> h:alice class=DECISION] go', origin: 'api' },
{ thread: T1, text: `${header('control-board', 'alice', T1)}\nbuzz`, origin: 'api' },
{ thread: T1, text: 'outside', at: '2026-09-13T00:00:00Z' },
{ thread: T1, text: 'an answer #9', role: 'assistant', origin: 'none' },
{ thread: T2, text: 'archived still counts #2' },
{ thread: T3, text: 'Sagebrush is not sage' },
{ thread: T3, text: `${header('sage', 'discord', T3)}\nnot a seat role`, origin: 'api' },
{ thread: T4, text: 'research this' },
{ thread: T5, text: '[from: SetSpark coordinator (x1) -> to: Discord Bot (x2)]\nfree text', origin: 'api' },
{ thread: 'import:claudeAgent:1', text: 'imported' },
{ thread: 't-deleted', text: 'deleted' },
{ thread: 't-other', text: 'other project' },
];
const write = (overrides = {}) => t3db(db, { root: f.real, projects: [['p1', f.real], ['p2', '/elsewhere']], threads, messages, ...overrides });
return { ...f, db, threads, messages, write };
}
test('T3: seat, archived, unmapped and Researcher threads count; imported, deleted and other-project threads do not', t => {
const f = t3Fixture(t); f.write();
const result = f.run(['--json', '--t3-db', f.db]);
assert.equal(result.status, 0, result.stderr);
const r = JSON.parse(result.stdout);
assert.deepEqual(r.seats, [
{ seat: 'alice', board: 2, agent: 3, human: 2 }, { seat: 'bob', board: 0, agent: 0, human: 1 },
{ seat: 'researcher', board: 0, agent: 0, human: 1 }, { seat: 't3:unmapped', board: 0, agent: 1, human: 2 },
]);
assert.deepEqual(r.pi, [{ seat: 'alice', board: 1, agent: 1, human: 1 }]);
assert.deepEqual(r.t3.database, { path: f.db, default: false });
assert.deepEqual(r.t3.seats, [
{ seat: 'alice', board: 1, agent: 2, human: 1, threads: [{ id: T1, title: 'Alice', archived: false }] },
{ seat: 'bob', board: 0, agent: 0, human: 1, threads: [{ id: T2, title: 'Bob in Claude', archived: true }] },
{ seat: 'researcher', board: 0, agent: 0, human: 1, threads: [{ id: T4, title: 'Researcher', archived: false }] },
]);
assert.deepEqual(r.t3.unmapped, { board: 0, agent: 1, human: 2, threads: [
{ id: T5, title: 'Discord Bot', archived: false }, { id: T3, title: 'Sagebrush', archived: false }] });
assert.deepEqual(r.t3.excluded, { importedThreads: 1, deletedThreads: 1, reasoningMessages: 0 });
// The free-text header counts as human; only the diagnostic shows it was sent through the API.
assert.deepEqual(r.t3.diagnostic, { humanSentThroughApi: 1, humanWithoutEvent: 0 });
assert.equal(r.totals.humanMessagesPerClosedIssue, 6);
assert.deepEqual(r.issues.map(x => [x.issue, x.seats]), [[1, ['alice']], [2, ['alice', 'bob']]]);
const text = f.run(['--t3-db', f.db]);
assert.equal(text.status, 0, text.stderr);
assert.ok(text.stdout.includes(`T3: read from ${f.db}, not the default`));
assert.match(text.stdout, /t3:unmapped \| 0 \| 1 \| 2/);
});
test('T3: the default path is read from HOME and prints no path line; --no-t3 says so', t => {
const f = t3Fixture(t); f.write();
rmSync(f.defaultDb); cpSync(f.db, f.defaultDb);
const json = JSON.parse(f.run(['--json']).stdout);
assert.deepEqual(json.t3.database, { path: f.defaultDb, default: true });
assert.equal(json.seats.at(-1).seat, 't3:unmapped');
const text = f.run(); assert.equal(text.status, 0, text.stderr); assert.doesNotMatch(text.stdout, /^T3:/m);
rmSync(path.join(f.home, '.t3'), { recursive: true });
const off = f.run(['--no-t3']); assert.equal(off.status, 0, off.stderr);
assert.match(off.stdout, /^T3: not read \(--no-t3\)$/m);
const offJson = JSON.parse(f.run(['--no-t3', '--json']).stdout);
assert.deepEqual(offJson.t3, { read: false }); assert.deepEqual(offJson.seats, [{ seat: 'alice', board: 1, agent: 1, human: 1 }]);
const both = f.run(['--no-t3', '--t3-db', f.db]); assert.equal(both.status, 1); assert.match(both.stderr, /cannot be combined/);
assert.equal(f.run(['--t3-db']).status, 1);
});
test('T3: a HOME with no database exits 1 and names --no-t3', t => {
const f = fixture(t); rmSync(path.join(f.home, '.t3'), { recursive: true });
const r = f.run(); assert.equal(r.status, 1); assert.equal(r.stdout, '');
assert.match(r.stderr, /T3 database unavailable: .*\.t3 is missing or unreadable; use --no-t3/);
});
test('T3: a file that is not a database exits 1 and names --no-t3', t => {
const f = fixture(t); writeFileSync(f.defaultDb, 'not sqlite'.repeat(100));
const r = f.run(); assert.equal(r.status, 1); assert.match(r.stderr, /T3 database cannot be read: .*\(SQLite \d+\); use --no-t3/);
});
test('T3: a seat thread renamed to another seat exits 1 naming thread, title and roles', t => {
const f = t3Fixture(t);
f.write({ messages: [...f.messages, { thread: T2, text: `${header('sage', 'alice', T2, ' class=INFO')}\nfor alice`, origin: 'api' }] });
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1);
assert.equal(r.stderr.trim(), `T3 header conflict: thread ${T2} "Bob in Claude" maps to bob, but a header addresses alice`);
});
test('T3: an unmapped thread addressed as a seat exits 1', t => {
const f = t3Fixture(t);
f.write({ messages: [...f.messages, { thread: T3, text: `${header('bob', 'Sage', T3)}\nhi`, origin: 'api' }] });
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1);
assert.match(r.stderr, /thread t-sagebrush "Sagebrush" maps to no seat, but a header addresses Sage/);
});
test('T3: a header to another thread id is not cross-checked', t => {
const f = t3Fixture(t);
f.write({ messages: [...f.messages, { thread: T2, text: `${header('sage', 'alice', T1)}\ncopied`, origin: 'api' }] });
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.equal(JSON.parse(r.stdout).t3.seats[1].agent, 1);
});
test('T3: no project, or two, for this root exits 1', t => {
const f = t3Fixture(t);
f.write({ projects: [['p1', `${f.real}-link`], ['p2', '/elsewhere']] });
let r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, /T3 has no project for .*symlink does not match/);
f.write({ projects: [['p1', f.real], ['p2', f.real]] });
r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, /T3 has more than one project for/);
f.write({ projects: [['p1', f.real], ['p2', f.real, '2026-09-01T00:00:00Z']] });
assert.equal(f.run(['--t3-db', f.db]).status, 0);
});
for (const [name, after, pattern] of [
['a removed column', ['alter table projection_threads drop column title'], /T3 schema changed: missing projection_threads.title/],
['a missing table', ['drop table projection_thread_messages'], /T3 schema changed: missing projection_thread_messages$/m],
]) test(`T3: ${name} exits 1 and names it`, t => {
const f = t3Fixture(t); f.write({ after, messages: [] });
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, pattern);
});
for (const [name, message, pattern] of [
['an unknown role', { role: 'system' }, /T3 message m\d+ in thread t-alice has an unknown role/],
['role reasoning without the reasoning: id prefix', { role: 'reasoning', id: 'x-1' }, /T3 message x-1 in thread t-alice has an unknown role/],
['non-text content', { text: null }, /has non-text content/],
['an unparseable created_at', { at: 'yesterday' }, /has an invalid created_at/],
]) test(`T3: a counted row with ${name} exits 1 without its text`, t => {
const f = t3Fixture(t);
f.write({ messages: [...f.messages, { thread: T1, text: 'secret words', ...message }] });
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, pattern); assert.doesNotMatch(r.stderr, /secret/);
});
test('T3: a reasoning summary is skipped before any check and counted as excluded', t => {
const f = t3Fixture(t); f.write();
const before = JSON.parse(f.run(['--json', '--t3-db', f.db]).stdout);
const same = after => {
for (const key of ['seats', 'pi', 'issues', 'totals']) assert.deepEqual(after[key], before[key]);
assert.deepEqual([after.t3.seats, after.t3.unmapped, after.t3.diagnostic], [before.t3.seats, before.t3.unmapped, before.t3.diagnostic]);
};
// Null text and a bad created_at would each refuse a counted row.
const summary = { thread: T1, role: 'reasoning', id: 'reasoning:summary:1', text: null, at: 'yesterday', origin: 'none' };
f.write({ messages: [...f.messages, summary] });
let r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.deepEqual(JSON.parse(r.stdout).t3.excluded, { importedThreads: 1, deletedThreads: 1, reasoningMessages: 1 });
same(JSON.parse(r.stdout));
// So would a header conflict, and a #N mention would add an issue seat.
f.write({ messages: [...f.messages, summary, { thread: T2, role: 'reasoning', id: 'reasoning:summary:2', text: `${header('sage', 'alice', T2)}\n#7`, origin: 'none' }] });
r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.equal(JSON.parse(r.stdout).t3.excluded.reasoningMessages, 2);
same(JSON.parse(r.stdout));
});
test('T3: a missing orchestration_events makes the diagnostic unknown and keeps the counts', t => {
const f = t3Fixture(t); f.write();
const before = JSON.parse(f.run(['--json', '--t3-db', f.db]).stdout);
f.write({ after: ['drop table orchestration_events'] });
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
const after = JSON.parse(r.stdout);
assert.deepEqual(after.t3.diagnostic, { humanSentThroughApi: 'unknown', humanWithoutEvent: 'unknown' });
assert.deepEqual(after.seats, before.seats); assert.deepEqual(after.totals, before.totals);
});
test('T3: a human message with no event counts in humanWithoutEvent', t => {
const f = t3Fixture(t);
f.write({ messages: [...f.messages, { thread: T1, text: 'typed, no event', origin: 'none' }] });
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.deepEqual(JSON.parse(r.stdout).t3.diagnostic, { humanSentThroughApi: 1, humanWithoutEvent: 1 });
});
const badEvent = payload => `insert into orchestration_events (stream_id, event_type, payload_json, metadata_json) values ('${T1}', 'thread.message-sent', '${payload}', '{}')`;
for (const [name, payload] of [['an unparseable event', '{bad'], ['an event with no string messageId', '{"messageId":7}']]) {
test(`T3: ${name} makes the diagnostic unknown and keeps the counts`, t => {
const f = t3Fixture(t); f.write();
const before = JSON.parse(f.run(['--json', '--t3-db', f.db]).stdout);
f.write({ after: [badEvent(payload)] });
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
const after = JSON.parse(r.stdout);
assert.deepEqual(after.t3.diagnostic, { humanSentThroughApi: 'unknown', humanWithoutEvent: 'unknown' });
assert.deepEqual(after.seats, before.seats);
});
}
test('T3: an error that is not from SQLite is rethrown, not reported as a database failure', async t => {
const f = t3Fixture(t); f.write();
// In process with an explicit path, so the real ~/.t3 stays closed. A null
// range makes inRange throw a TypeError inside the read transaction.
await assert.rejects(readT3(f.real, null, { dbPath: f.db, isDefault: false }), TypeError);
});
for (const link of ['.t3', '.t3/userdata', '.t3/userdata/state.sqlite']) test(`T3: a symlink at ~/${link} exits 1`, t => {
const f = fixture(t), target = path.join(f.home, 'real', link);
mkdirSync(path.dirname(target), { recursive: true });
cpSync(path.join(f.home, link), target, { recursive: true });
rmSync(path.join(f.home, link), { recursive: true }); symlinkSync(target, path.join(f.home, link));
const r = f.run(); assert.equal(r.status, 1); assert.match(r.stderr, new RegExp(`${link.replaceAll('.', '\\.')} is a symlink; use --no-t3`));
});
test('T3: with --t3-db, a symlinked file or directory exits 1', t => {
const f = t3Fixture(t); f.write();
const file = path.join(f.home, 'file-link.sqlite'); symlinkSync(f.db, file);
let r = f.run(['--t3-db', file]); assert.equal(r.status, 1); assert.match(r.stderr, /file-link.sqlite is a symlink/);
const dir = path.join(f.home, 'dir-link'); symlinkSync(path.dirname(f.db), dir);
r = f.run(['--t3-db', path.join(dir, 't3.sqlite')]); assert.equal(r.status, 1); assert.match(r.stderr, /dir-link is a symlink/);
});
// WAL states. The CLI reads with mode=ro; it may create -wal and -shm but must
// never change the main file.
const sha = file => execFileSync('sha256sum', [file], { encoding: 'utf8' }).split(' ')[0];
const humans = r => JSON.parse(r.stdout).t3.seats.find(s => s.seat === 'alice').human;
const asRoot = process.getuid?.() === 0;
function killedWriter(db) {
// A writer that commits into the WAL and dies without a checkpoint.
const code = `const { DatabaseSync } = require('node:sqlite'); const db = new DatabaseSync(${JSON.stringify(db)});
db.exec('pragma wal_autocheckpoint=0');
db.prepare("insert into projection_thread_messages values ('late', 't-alice', 'user', 'late human', '2026-09-08T13:00:00Z')").run();
process.kill(process.pid, 'SIGKILL');`;
const r = spawnSync(process.execPath, ['-e', code]);
assert.equal(r.signal, 'SIGKILL');
rmSync(`${db}-shm`);
}
function inReadOnlyDir(dir, check) {
execFileSync('chmod', ['0555', dir]);
try { check(); } finally { execFileSync('chmod', ['0755', dir]); }
}
test('T3 WAL: the newest message only in -wal, writer attached, is counted', t => {
const f = t3Fixture(t), writer = f.write({ keepOpen: true });
t.after(() => writer.close());
writer.exec('pragma wal_autocheckpoint=0');
addMessage(writer, { thread: T1, text: 'newest', at: '2026-09-08T13:00:00Z' });
const main = sha(f.db);
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.equal(humans(r), 2); assert.equal(sha(f.db), main);
});
test('T3 WAL: stopped cleanly, counts are correct and the main file is unchanged', t => {
const f = t3Fixture(t); f.write();
assert.throws(() => readFileSync(`${f.db}-wal`));
const main = sha(f.db);
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.equal(humans(r), 1); assert.equal(sha(f.db), main);
});
test('T3 WAL: -wal without -shm in a writable directory is read', t => {
const f = t3Fixture(t); f.write(); killedWriter(f.db);
const main = sha(f.db);
const r = f.run(['--json', '--t3-db', f.db]); assert.equal(r.status, 0, r.stderr);
assert.equal(humans(r), 2); assert.equal(sha(f.db), main);
});
test('T3 WAL: -wal without -shm in a read-only directory exits 1', { skip: asRoot && 'mode bits do not bind root' }, t => {
const f = t3Fixture(t); f.write(); killedWriter(f.db);
inReadOnlyDir(path.dirname(f.db), () => {
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, /cannot be read: .*\(SQLite 14\); use --no-t3/);
});
});
test('T3 WAL: stopped cleanly in a read-only directory exits 1', { skip: asRoot && 'mode bits do not bind root' }, t => {
const f = t3Fixture(t); f.write();
inReadOnlyDir(path.dirname(f.db), () => {
const r = f.run(['--t3-db', f.db]); assert.equal(r.status, 1); assert.match(r.stderr, /cannot be read: .*\(SQLite 1544\); use --no-t3/);
});
});
test('T3: a lock held past the 5 s busy timeout exits 1 and names --no-t3', t => {
const f = t3Fixture(t), writer = f.write({ keepOpen: true });
t.after(() => writer.close());
writer.exec('pragma locking_mode=exclusive'); writer.exec('begin exclusive');
addMessage(writer, { thread: T1, text: 'held' });
const started = Date.now(), r = f.run(['--t3-db', f.db]);
writer.exec('commit');
assert.equal(r.status, 1); assert.match(r.stderr, /cannot be read: .*\(SQLite 5\); use --no-t3/);
assert.ok(Date.now() - started >= 4500, 'the reader waited for the busy timeout');
});