import test from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Store } from '../src/store.mjs'; import { Broker } from '../src/broker.mjs'; // Row 270 (264c, #1775, T1 item 7 D6): session.list's `compactions` key, from the host's ledger // reader (runtime.mjs gives the seat's compactionCount). The reader itself is // packages/seat/tests/compactions.test.mjs's. const shipped = (name) => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8')); const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'chief' }, roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } }, assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } }, pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } }, }, definitions: { 'chief-of-staff': {}, assistant: { messaging: shipped('assistant').messaging }, pm: {} }, projects: { stack: { root: '/nonexistent' } }, }, }; function setup(t, options = {}) { const root = mkdtempSync(join(tmpdir(), 'bus-compactions-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const b = new Broker({ store, businesses, ...options }); const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const call = (verb, args = {}) => b.request(human, { verb, args }); const project = call('project.create', { name: 'stack work', repo: 'stack' }).id; const A = call('thread.create', { project, name: 'alpha' }).id; return { store, call, project, A }; } test('session.list rows carry the reader count for each session, with its store identity', (t) => { const asked = []; const f = setup(t, { compactions: (q) => { asked.push(q); return { count: asked.length, known: true, reason: null, at: '2026-10-22T12:00:00.000Z' }; }, }); const S = f.call('session.create', { thread: f.A, name: 'second', op: 'op1' }).id; const rows = f.call('session.list', { thread: f.A }); assert.deepEqual(rows.map((r) => [r.id, r.compactions]), [ [rows[0].id, { count: 1, known: true, reason: null, at: '2026-10-22T12:00:00.000Z' }], [S, { count: 2, known: true, reason: null, at: '2026-10-22T12:00:00.000Z' }], ]); assert.deepEqual(asked, rows.map((r) => ({ business: 'demo', thread: f.A, session: r.id, legacy: false }))); assert.ok(rows.every((r) => !('op' in r))); }); // A legacy session (op legacy-3l, from the 3k-3l migration, its id the thread's) keeps the // thread's store. The thread is made by hand with the session the migration gives it, as // message-filter.test.mjs does; sessions is append-only. test('a legacy session is asked for with legacy true', (t) => { const asked = []; const f = setup(t, { compactions: (q) => (asked.push(q), { count: 0, known: true, reason: null, at: null }) }); const L = randomUUID(); const at = '2026-10-21T00:00:00.000Z'; f.store.run('INSERT INTO threads(id,at,business,project,instance,owner,name) VALUES(?,?,?,?,?,?,?)', L, at, 'demo', f.project, 'assistant', 'jason', 'legacy'); f.store.run("INSERT INTO sessions(at,business,thread,id,instance,owner,name,op) SELECT at,business,id,id,instance,owner,'default','legacy-3l' FROM threads WHERE id=?", L); f.store.run("INSERT INTO session_defaults(at,thread,session,by) SELECT at,id,id,'migration-3l' FROM threads WHERE id=?", L); f.call('session.list', { thread: L }); assert.deepEqual(asked, [{ business: 'demo', thread: L, session: L, legacy: true }]); }); test('without a reader the count is unknown, never 0; a reader that throws is store-unreadable', (t) => { const f = setup(t); assert.deepEqual(f.call('session.list', { thread: f.A }).map((r) => r.compactions), [{ count: null, known: false, reason: 'not-collected', at: null }]); const g = setup(t, { compactions: () => { throw new Error('EACCES'); }, }); assert.deepEqual(g.call('session.list', { thread: g.A }).map((r) => r.compactions), [{ count: null, known: false, reason: 'store-unreadable', at: null }]); }); test('compactions must be a function', (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-compactions-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); assert.throws(() => new Broker({ store, businesses, compactions: {} }), { name: 'TypeError', message: 'compactions must be a function' }); });