import test from 'node:test'; import assert from 'node:assert/strict'; import { existsSync, mkdirSync, 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'; import { startBroker } from '../src/runtime.mjs'; import { businessDoc, FAR_DATE, rolesCopy, systemFor, writeJson } from '../../organization/tests/helpers.mjs'; // Row 213 (C2-08, #1714, Case 2 design 5.1): a creative instance talks with the user only, and it has // no decision channel (M6). The messaging lists are read from the shipped role files. const shipped = (name) => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8')); const within = { authority: { withinRole: ['message.send'], crossRole: [] } }; const roles = { chief: { definition: 'chief-of-staff', ...within }, assistant: { definition: 'assistant', ...within }, pm: { definition: 'pm', ...within }, bean: { definition: 'companion', ...within }, quill: { definition: 'proofreader', ...within }, }; const definitions = { 'chief-of-staff': {}, assistant: { messaging: shipped('assistant').messaging }, pm: {}, companion: { messaging: shipped('companion').messaging }, proofreader: { messaging: shipped('proofreader').messaging }, }; function setup(t) { const root = mkdtempSync(join(tmpdir(), 'bus-creative-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const b = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'chief' }, roles, definitions, projects: { stack: { root: '/nonexistent' } }, vars: { 'bus.tester': true }, } } }); const caps = {}; caps.human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const project = b.request(caps.human, { verb: 'project.create', args: { name: 'demo' } }).id; const thread = b.request(caps.human, { verb: 'thread.create', args: { project, name: 'main', instance: 'assistant' } }).id; const threads = { assistant: thread }; for (const role of Object.keys(roles)) { if (['bean', 'quill'].includes(role)) threads[role] = b.request(caps.human, { verb: 'thread.create', args: { project, name: role, instance: role } }).id; caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi', ...(threads[role] ? { thread: threads[role] } : {}) }); b.request(caps[role], { verb: 'role.claim' }); } caps.tester = b.bindTester({ business: 'demo' }); const call = (who, verb, args = {}) => b.request(caps[who], { verb, args }); const count = (sql) => store.get(sql).n; return { call, count, store, threads }; } const raise = { action: 'policy.change', question: 'Change?', options: [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }], recommendation: 'yes', blocking: false }; test('M6: a creative raise refuses decision-not-allowed, cited or not, and no decision row appears; the assistant still raises', (t) => { const { call, count } = setup(t); for (const who of ['bean', 'quill']) { assert.throws(() => call(who, 'decision.raise', raise), { code: 'decision-not-allowed' }, who); // The refusal comes before the spec-cited path, so a cited raise never reaches the project. assert.throws(() => call(who, 'decision.raise', { ...raise, project: 'stack', spec_path: 'spec/north-star.md', spec_commit: 'a'.repeat(40) }), { code: 'decision-not-allowed' }, `${who} cited`); } assert.equal(count('SELECT count(*) n FROM decisions'), 0); const d = call('assistant', 'decision.raise', raise); assert.match(String(d.id), /./); assert.equal(count('SELECT count(*) n FROM decisions'), 1); }); test('a creative instance sends only to the user and hears only from the user', (t) => { const { call, count, threads } = setup(t); for (const who of ['bean', 'quill']) { call(who, 'message.send', { to: 'human', body: 'hello' }); for (const to of ['chief', 'assistant', 'pm', 'bean', 'quill'].filter((n) => n !== who)) { assert.throws(() => call(who, 'message.send', { to, body: 'hello' }), { code: 'message-recipient-refused' }, `${who} to ${to}`); } for (const from of ['chief', 'pm']) { assert.throws(() => call(from, 'message.send', { to: who, body: 'hello' }), { code: 'message-sender-refused' }, `${from} to ${who}`); } call('human', 'thread.send', { thread: threads[who], body: 'hello' }); } assert.equal(count("SELECT count(*) n FROM messages WHERE from_role IN ('bean','quill')"), 2); assert.equal(count("SELECT count(*) n FROM messages WHERE to_role IN ('bean','quill')"), 2); }); // M10 through the broker (webby-0040 B1): the broker names the actor from the session, so these cases // pin that a chief session reaches the writer as chief-of-staff.chief and a tester session as tester. // A real organization file and writer, as tester-writes.test.mjs. async function runtime(t) { const root = mkdtempSync(join(tmpdir(), 'bus-creative-m10-')); const rolesDir = rolesCopy(root); const doc = businessDoc(root, 'demo'); doc.plugins = { ...doc.plugins, harness: ['pi'] }; doc.roles.bean = { definition: 'companion', credentials: { gitea: { store: true, rotateBy: FAR_DATE } } }; for (const r of Object.values(doc.roles)) r.vars = { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high', ...r.vars }; // Only bean names the sonnet key, so the chief may still change every other key. for (const [name, r] of Object.entries(doc.roles)) if (name !== 'bean') r.vars.model = 'claude-opus-5-5'; const file = join(root, 'businesses/demo.json'); writeJson(file, doc); mkdirSync(join(root, 'data'), { mode: 0o700 }); const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, plugins: { harness: ['pi'] }, roles: { chief: { definition: 'chief-of-staff', authority: shipped('chief-of-staff').authority }, bean: { definition: 'companion', ...within } }, }, }, organization: { dir: root, rolesDir, system: systemFor(root) }, }); t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); }); const b = bus.broker; const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const tester = b.bindTester({ business: 'demo' }); const chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' }); b.request(chief, { verb: 'role.claim' }); const call = (cap, verb, args = {}) => b.request(cap, { verb, args }); const history = join(root, 'businesses/.history/demo/changes.jsonl'); const log = () => (existsSync(history) ? readFileSync(history, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : []); const bytes = () => readFileSync(file, 'utf8'); return { bus, human, tester, chief, call, log, bytes }; } test('M10 through the broker: the chief and the tester are refused on bean, the human\'s limits.set of its models key works', async (t) => { const { human, tester, chief, call, log, bytes } = await runtime(t); const { revision, models } = call(human, 'limits'); const changed = { ...models, 'claude-sonnet-5-5': { ...models['claude-sonnet-5-5'], model: 'claude-sonnet-5-5-next' } }; const before = bytes(); // The chief of staff's organization.set reaches the writer as chief-of-staff.chief and is refused there. assert.throws(() => call(chief, 'organization.set', { revision, models: changed }), { code: 'organization-creative-human-only' }); assert.throws(() => call(chief, 'organization.set', { revision, instances: { bean: 'claude-opus-5-5' } }), { code: 'organization-creative-instance' }); // The tester never reaches the writer through these two verbs (webby-0040 N5). assert.throws(() => call(tester, 'organization.set', { revision, models: changed }), { code: 'tester-refused' }); assert.throws(() => call(tester, 'limits.set', { revision, models: changed }), { code: 'tester-refused' }); // Its instances.set and routes.set do reach the writer, as tester, and are refused there. assert.throws(() => call(tester, 'instances.set', { revision, instance: 'bean', op: 'change', vars: { thinking: 'low' } }), { code: 'organization-creative-human-only' }); assert.throws(() => call(tester, 'instances.set', { revision, instance: 'muse', op: 'add', definition: 'ideas' }), { code: 'organization-creative-human-only' }); assert.throws(() => call(tester, 'routes.set', { revision, routes: { bean: { harness: 'pi', model: 'claude-opus-5-5', thinking: 'high', fallback: null } } }), { code: 'organization-creative-human-only' }); assert.equal(bytes(), before); assert.deepEqual(log().filter((e) => e.state === 'committed'), []); // The human's limits.set of the same change lands, logged with the human as actor. call(human, 'limits.set', { revision, models: changed }); assert.equal(JSON.parse(bytes()).launch.models['claude-sonnet-5-5'].model, 'claude-sonnet-5-5-next'); assert.deepEqual(log().filter((e) => e.state === 'committed').map((e) => e.actor), ['jason']); }); // Row 256 (#1761, REQ-COS-3, T6): the human deletes bean through the verb and the launcher's pass, on // the real writer, so the actor reaches the delete check at both. The chief is refused human-required // and the tester tester-refused at the verb, so neither reaches the writer. Each reader's instances // view carries its own endProtected. test('row 256 T6 the human deletes a creative instance through the broker; the chief and the tester are refused at the verb', async (t) => { const { bus, human, tester, chief, call, log, bytes } = await runtime(t); assert.equal(call(human, 'instances').instances.bean.endProtected, false); assert.equal(call(tester, 'instances').instances.bean.endProtected, true); const { revision } = call(human, 'instances'); const before = bytes(); assert.throws(() => call(chief, 'instances.set', { revision, instance: 'bean', op: 'delete' }), { code: 'human-required' }); assert.throws(() => call(tester, 'instances.set', { revision, instance: 'bean', op: 'delete' }), { code: 'tester-refused' }); assert.equal(bytes(), before); // The verb's check runs instances.check with the human as actor, and records the request. const { request } = call(human, 'instances.set', { revision, instance: 'bean', op: 'delete' }); assert.equal(bytes(), before); assert.equal(bus.lifecycleTake({ business: 'demo', request }).go, true); // The launcher's pass commits the delete with the request's by, the human. const { transaction } = bus.lifecycleDelete({ business: 'demo', request }); const file = JSON.parse(bytes()); assert.equal(Object.hasOwn(file.roles, 'bean'), false); assert.deepEqual(file.retired, ['bean']); const committed = log().filter((e) => e.state === 'committed'); assert.deepEqual(committed.map((e) => e.actor), ['jason']); assert.equal(committed[0].transaction, transaction); });