import test from 'node:test'; import assert from 'node:assert/strict'; import { fork } from 'node:child_process'; import { mkdtempSync, rmSync, existsSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { once } from 'node:events'; import { Client } from '../src/client.mjs'; const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'cto' }, roles: { cto: { authority: { withinRole: ['message.send'], crossRole: [] } } }, }, }; function launch(t) { const child = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'], }); let logs = ''; child.stdout.on('data', (b) => (logs += b)); child.stderr.on('data', (b) => (logs += b)); t.after(() => { if (child.exitCode === null) child.kill('SIGKILL'); }); return { child, logs: () => logs }; } async function boot(child, config) { const reply = Promise.race([ once(child, 'message'), once(child, 'exit').then(() => { throw Error('exited-before-reply'); }), ]); child.send({ op: 'boot', config }); return (await reply)[0]; } test('broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-process-')); t.after(() => rmSync(root, { recursive: true, force: true })); const { child } = launch(t); const ready = await boot(child, { dataRoot: root, businesses, launches: [{ business: 'demo', role: 'cto', run: 'r1', harness: 'pi', pid: process.pid, startTime: '1' }], readers: ['demo'], }); assert.equal(ready.ok, true); const agent = new Client({ path: ready.path, cap: ready.launches[0].cap }); await agent.call('role.claim'); const reader = new Client({ path: ready.path, cap: ready.readers[0].cap }); assert.equal((await reader.call('agents'))[0].holder_run, 'r1'); await assert.rejects(reader.call('launch.revoke'), /read-only/); await assert.rejects( new Client({ path: ready.path, human: { business: 'demo', pid: process.pid, startTime: '1', nonce: 'f'.repeat(64) }, }).call('launch.revoke'), /human-required/, ); const exit = once(child, 'exit'); child.send({ op: 'close' }); assert.equal((await exit)[0], 0); assert.equal(existsSync(join(root, 'bus/writer.lock')), false); }); test('startup token refusal returns safe code without value or partial listening broker', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-boot-')); t.after(() => rmSync(root, { recursive: true, force: true })); const data = join(root, 'data'); mkdirSync(data); const secret = 'fixture-token-never-in-db-948723'; const file = join(root, 'secret'); writeFileSync(file, secret, { mode: 0o644 }); const config = structuredClone(businesses); config.demo.roles.cto.credentials = { gitea: { file, rotateBy: '2099-01-01' } }; const { child, logs } = launch(t); const exit = once(child, 'exit'); const ready = await boot(child, { dataRoot: data, businesses: config, launches: [], readers: [] }); assert.equal(ready.ok, false); assert.equal(ready.error, 'credential-file'); assert.equal((await exit)[0], 2); assert.ok(!logs().includes(secret)); assert.equal(existsSync(join(data, 'bus/broker.sock')), false); }); test('loaded fixture token is absent from socket replies and SQLite, including refusal evidence', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-secret-')); t.after(() => rmSync(root, { recursive: true, force: true })); const data = join(root, 'data'); mkdirSync(data); const token = 'fixture-opaque-token-e9c39140'; const file = join(root, 'token'); writeFileSync(file, token, { mode: 0o600 }); const config = structuredClone(businesses); config.demo.roles.cto.credentials = { gitea: { file, rotateBy: '2099-01-01' } }; const { child, logs } = launch(t); const ready = await boot(child, { dataRoot: data, businesses: config, launches: [{ business: 'demo', role: 'cto', run: 'r2', harness: 'pi', pid: process.pid, startTime: '1' }], }); assert.equal(ready.ok, true); const c = new Client({ path: ready.path, cap: ready.launches[0].cap }); await c.call('role.claim'); await assert.rejects(c.call('message.send', { to: 'cto', body: token }), /credential-leak/); const exit = once(child, 'exit'); child.send({ op: 'close' }); await exit; assert.ok(!readFileSync(join(data, 'bus/bus.sqlite')).includes(Buffer.from(token))); assert.ok(!logs().includes(token)); }); test('killed broker leaves an explicit stale lock; another process cannot silently reclaim it', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-crash-')); t.after(() => rmSync(root, { recursive: true, force: true })); const first = launch(t); const config = { dataRoot: root, businesses, launches: [] }; assert.equal((await boot(first.child, config)).ok, true); const killed = once(first.child, 'exit'); first.child.kill('SIGKILL'); await killed; assert.equal(existsSync(join(root, 'bus/writer.lock')), true); const second = launch(t), ended = once(second.child, 'exit'); const refusal = await boot(second.child, config); assert.equal(refusal.ok, false); assert.equal((await ended)[0], 2); assert.equal(existsSync(join(root, 'bus/writer.lock')), true); }); test('trusted host registers later launches; socket clients never have a registration verb', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-add-')); t.after(() => rmSync(root, { recursive: true, force: true })); const { child } = launch(t); const ready = await boot(child, { dataRoot: root, businesses, launches: [] }); assert.equal(ready.ok, true); const message = once(child, 'message'); child.send({ op: 'bindLaunch', record: { business: 'demo', role: 'cto', run: 'later', harness: 'pi', pid: process.pid, startTime: '1' }, }); const bound = (await message)[0]; assert.equal(bound.ok, true); const c = new Client({ path: ready.path, cap: bound.launch.cap }); await c.call('role.claim'); await assert.rejects(c.call('bindLaunch', { role: 'cto' }), /unknown-verb/); const exit = once(child, 'exit'); child.send({ op: 'close' }); assert.equal((await exit)[0], 0); }); test('runtime excludes declared project roots even when host supplies no repoRoots', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-project-token-')); t.after(() => rmSync(root, { recursive: true, force: true })); const data = join(root, 'data'); mkdirSync(data); const project = join(root, 'project'); mkdirSync(project); const file = join(project, 'token'); writeFileSync(file, 'fixture-do-not-load-from-project', { mode: 0o600 }); const config = structuredClone(businesses); config.demo.projects = { stack: { root: project } }; config.demo.roles.cto.credentials = { gitea: { file, rotateBy: '2099-01-01' } }; const { child } = launch(t), exit = once(child, 'exit'); const result = await boot(child, { dataRoot: data, businesses: config }); assert.equal(result.ok, false); assert.equal(result.error, 'credential-location'); assert.equal((await exit)[0], 2); }); test('a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-bind-refusal-')); t.after(() => rmSync(root, { recursive: true, force: true })); const { child } = launch(t); const record = { business: 'demo', role: 'cto', run: 'one', harness: 'pi', pid: process.pid, startTime: '1', }; const ready = await boot(child, { dataRoot: root, businesses, launches: [record] }); assert.equal(ready.ok, true); const client = new Client({ path: ready.path, cap: ready.launches[0].cap }); await client.call('role.claim'); const reply = once(child, 'message'); child.send({ op: 'bindLaunch', record }); assert.deepEqual((await reply)[0], { ok: false, error: 'duplicate-run' }); assert.equal((await client.call('agents'))[0].holder_run, 'one'); const bad = once(child, 'message'), exit = once(child, 'exit'); child.send({ op: 'not-a-protocol-verb' }); assert.equal((await bad)[0].ok, false); assert.equal((await exit)[0], 2); });