// C2-03a: library.sync on the bus and the bus host's 60-second timer, with made-up local data. import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createServer } from 'node:net'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { Store } from '../src/store.mjs'; import { Broker, LIBRARY_VERBS } from '../src/broker.mjs'; import { startBroker } from '../src/runtime.mjs'; import { gitEnv } from '../../seat/src/checkout.mjs'; import { mappingEntry, repo } from '../../library/tests/helpers.mjs'; import * as library from '../../library/src/index.mjs'; const TOKEN = 'fixture-token-9b2e-made-up-0043'; const STATE = { version: 1, branch: 'main', served: 'a'.repeat(40), lastFetch: '2026-10-21T12:00:00.000Z', lastError: null, diverged: null, problems: [] }; function world(t, adapter) { const root = mkdtempSync(join(tmpdir(), 'bus-library-sync-')), store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true }, projects: { books: { ...mappingEntry(), user: 'books-bot' } }, arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } }; const broker = new Broker({ store, businesses: { acme: business }, library: adapter }); const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true }); const agent = broker.bindLaunch({ business: 'acme', role: 'coder', run: 'library-sync-agent', harness: 'pi' }); const reader = broker.bindReader({ business: 'acme' }), tester = broker.bindTester({ business: 'acme' }); const call = (cap, verb, args = {}) => Promise.resolve().then(() => broker.request(cap, { verb, args })); const refusals = () => store.all("SELECT body FROM events WHERE kind='action.refused'").map(row => JSON.parse(row.body).code); return { store, broker, human, agent, reader, tester, call, refusals }; } const adapter = (sync) => ({ readFile: () => {}, status: () => {}, sync, LibraryError: library.LibraryError }); test('C2-03a library.sync: every agent, the tester and a reader are refused before the adapter runs', async t => { let touched = 0; const f = world(t, adapter(async () => { touched++; return { state: STATE, code: null }; })); await assert.rejects(f.call(f.agent, 'library.sync', { project: 'books' }), { code: 'human-required' }); await assert.rejects(f.call(f.tester, 'library.sync', { project: 'books' }), { code: 'human-required' }); await assert.rejects(f.call(f.reader, 'library.sync', { project: 'books' }), { code: 'read-only' }); assert.equal(touched, 0); assert.ok(LIBRARY_VERBS.includes('library.sync'), 'the server awaits it like the other library verbs'); }); test('C2-03a library.sync: the human fetches, accept passes through, and a held sync is a recorded refusal', async t => { const seen = []; let code = null; const f = world(t, adapter(async (context, options) => { seen.push({ context, options }); return { state: STATE, code }; })); assert.deepEqual(await f.call(f.human, 'library.sync', { project: 'books' }), { project: 'books', ...STATE }); assert.equal(seen[0].context.business.id, 'acme'); assert.equal(seen[0].context.project, 'books'); assert.deepEqual(seen[0].options, { accept: null }); await f.call(f.human, 'library.sync', { project: 'books', accept: 'b'.repeat(40) }); assert.deepEqual(seen[1].options, { accept: 'b'.repeat(40) }); for (const held of ['library-diverged', 'library-fetch-failed', 'library-credential-missing']) { code = held; await assert.rejects(f.call(f.human, 'library.sync', { project: 'books' }), { code: held }); } assert.deepEqual(f.refusals(), ['library-diverged', 'library-fetch-failed', 'library-credential-missing']); for (const args of [{}, { project: 'books', extra: 1 }, { project: 'books', commit: 'b'.repeat(40) }]) await assert.rejects(f.call(f.human, 'library.sync', args), { code: 'invalid-request' }); }); test('C2-03a library.sync: a LibraryError keeps its code and a host without sync is not configured', async t => { const f = world(t, adapter(async () => { throw new library.LibraryError('library-accept-stale', 'made up'); })); await assert.rejects(f.call(f.human, 'library.sync', { project: 'books', accept: 'c'.repeat(40) }), { code: 'library-accept-stale' }); const none = world(t, adapter(undefined)); await assert.rejects(none.call(none.human, 'library.sync', { project: 'books' }), { code: 'project-not-configured' }); }); test('C2-03a runtime: a librarySync from a boot config, with no function, is refused', async t => { const root = mkdtempSync(join(tmpdir(), 'bus-library-boot-')); t.after(() => rmSync(root, { recursive: true, force: true })); for (const librarySync of [{}, { intervalMs: 1 }, { command: 'git' }, { command: () => {}, intervalMs: 0 }, { command: () => {}, configDir: 7 }]) await assert.rejects(startBroker({ dataRoot: root, businesses: {}, librarySync }), { code: 'invalid-host-request' }); }); test('C2-03a runtime: the timer syncs each entry with a user at start and on each tick, logs codes only, and stops on close', { timeout: 20000 }, async t => { const f = repo(t), dataRoot = join(f.root, 'data'), configDir = join(f.root, 'config'); mkdirSync(dataRoot, { mode: 0o700 }); mkdirSync(join(configDir, 'secrets', 'acme'), { recursive: true, mode: 0o700 }); writeFileSync(join(configDir, 'secrets', 'acme', library.SLOT), TOKEN + '\n', { mode: 0o600 }); const business = { id: 'acme', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } }, projects: { books: { ...mappingEntry(), user: 'books-bot' }, quiet: mappingEntry() } }; const calls = []; // A git that succeeds without fetching: the remote-tracking ref stays absent, so each pass fails. const command = (kind, url, opts) => { calls.push({ kind, url, opts }); return { argv: ['--version'], env: gitEnv() }; }; const lines = [], write = process.stderr.write; process.stderr.write = function (chunk, ...rest) { if (String(chunk).startsWith('library: ')) { lines.push(String(chunk)); return true; } return write.call(this, chunk, ...rest); }; t.after(() => { process.stderr.write = write; }); const runtime = await startBroker({ dataRoot, businesses: { acme: business }, librarySync: { command, intervalMs: 50, configDir } }); const deadline = Date.now() + 10000; while (calls.length < 2 && Date.now() < deadline) await new Promise(r => setTimeout(r, 10)); await runtime.close(); const after = calls.length; assert.ok(after >= 2, 'a pass at start and at least one tick'); assert.ok(calls.every(c => c.kind === 'fetch' && c.opts.slot === join(configDir, 'secrets', 'acme', library.SLOT) && c.opts.user === 'books-bot')); assert.equal(new Set(calls.map(c => c.opts.mirror)).size, 1, 'the entry with no user is never synced'); await new Promise(r => setTimeout(r, 200)); assert.equal(calls.length, after, 'no pass after close'); assert.ok(lines.length >= 1); for (const line of lines) { assert.deepEqual(JSON.parse(line.slice('library: '.length)), { event: 'library-sync-held', business: 'acme', project: 'books', code: 'library-fetch-failed' }); assert.equal(line.includes(TOKEN), false); } const mirror = library.mirrorPath({ business, project: 'books', dataRoot }); assert.equal((await library.readSyncState(mirror, { branch: 'main' })).lastError.code, 'library-fetch-failed'); }); // Row 254 (#1759): the human transport waits past the server's 120 s library hold for library.sync // only. A stand-in bus answers each request after 6 s, past the 5 s default every other verb keeps. test('row 254 human-cli: library.sync outlasts the 5 s client default, and library.status does not', { timeout: 20000 }, async t => { const root = mkdtempSync(join(tmpdir(), 'bus-library-cli-')); const socket = join(root, 'bus.sock'), timers = new Set(); const server = createServer(conn => { let input = ''; conn.on('error', () => {}); conn.on('data', b => { input += b; if (!input.includes('\n')) return; const { verb } = JSON.parse(input); timers.add(setTimeout(() => { if (!conn.destroyed) conn.end(JSON.stringify({ ok: true, result: { verb } }) + '\n'); }, 6000)); }); }); await new Promise(r => server.listen(socket, r)); t.after(() => { for (const timer of timers) clearTimeout(timer); server.close(); rmSync(root, { recursive: true, force: true }); }); const cli = fileURLToPath(new URL('../src/human-cli.mjs', import.meta.url)); const run = verb => new Promise(resolve => { const child = spawn(process.execPath, [cli, socket], { stdio: ['pipe', 'pipe', 'pipe'] }); let out = '', err = ''; child.stdout.on('data', b => { out += b; }); child.stderr.on('data', b => { err += b; }); child.on('close', status => resolve({ status, out, err })); child.stdin.end(JSON.stringify({ business: 'acme', verb, args: { project: 'books' } }) + '\n'); }); const [sync, status] = await Promise.all([run('library.sync'), run('library.status')]); assert.equal(sync.status, 0, sync.err); assert.deepEqual(JSON.parse(sync.out), { verb: 'library.sync' }); assert.equal(status.status, 2); assert.equal(status.err.trim(), 'outcome-unknown'); });