import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { fixture, git } from './spec-fixtures.mjs'; // Row 143 (#1637, REQ-PROJ-4): a human save of spec/north-star.md through project.file.save sends one notice // to each pm and cto instance. A BLOCK goes to the chief of staff and, through its relay, to the assistant. // The assistant's lists are the shipped roles/assistant.json's, so the chain runs under the lists the stack runs. const shipped = name => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8')); const within = { authority: { withinRole: ['message.send'], crossRole: [] } }; const SPEC = 'spec/north-star.md'; const reject = (promise, code) => assert.rejects(Promise.resolve(promise), { code }); // message.send refuses synchronously, so its calls come in as thunks. const refuse = (thunk, code) => assert.rejects(async () => thunk(), { code }); async function setup(t, { running = ['chief', 'pm', 'cto'], thread = true } = {}) { const f = await fixture(t, { roles: { chief: { definition: 'chief-of-staff', ...within }, pm: { definition: 'pm', ...within }, cto: { definition: 'cto', ...within }, assistant: { definition: 'assistant', ...within }, } }); f.business.definitions = { assistant: { messaging: shipped('assistant').messaging } }; f.cap = Object.fromEntries(running.map(role => [role, role === 'chief' ? f.chief : f.agent(role)])); if (thread) { const project = f.call(f.human, 'project.create', { name: 'stack work', repo: 'stack' }).id; f.threadId = f.call(f.human, 'thread.create', { project, name: 'alpha' }).id; f.cap.assistant = f.broker.bindLaunch({ business: 'demo', role: 'assistant', run: 'assistant-run', harness: 'pi', thread: f.threadId }); f.broker.request(f.cap.assistant, { verb: 'role.claim' }); } f.notices = () => f.store.all("SELECT * FROM messages WHERE from_role='_bus' ORDER BY seq"); f.noticeTo = role => f.notices().find(n => n.to_role === role); f.threadOf = id => f.store.get('SELECT thread FROM message_threads WHERE message=?', id)?.thread ?? null; f.head = async () => (await f.call(f.human, 'project.file', { project: 'stack', path: SPEC })).commit; f.saveSpec = async content => f.call(f.human, 'project.file.save', { project: 'stack', path: SPEC, base: await f.head(), content }); f.send = (role, to, args) => f.call(f.cap[role], 'message.send', { to, body: 'text', ...args }); return f; } test('a human spec save sends one notice to each pm and cto, naming the commit, and each commit sends its own', async t => { const f = await setup(t); const saved = await f.saveSpec('# Edited\n'); const sent = f.notices(); assert.deepEqual(sent.map(n => n.to_role).sort(), ['cto', 'pm']); for (const n of sent) { assert.equal(n.from_run, 'bus'); assert.equal(n.class, 'RESULT'); assert.equal(n.in_reply_to, null); assert.ok(n.body.startsWith(`The human saved ${SPEC} in project stack: commit ${saved.commit}`)); assert.ok(n.body.includes(`from the editor's base ${f.commit}`) && n.body.includes('read_spec') && n.body.includes(`in_reply_to ${n.id}`)); assert.equal(f.threadOf(n.id), null); // Routed to the run that holds the role, as any message is, so the runner takes a turn on it. assert.ok(f.store.get('SELECT holder_run FROM deliveries WHERE message=?', n.id)?.holder_run); } for (const role of ['pm', 'cto']) assert.deepEqual((await f.call(f.cap[role], 'message.receive')).map(m => m.id), [f.noticeTo(role).id]); assert.deepEqual(await f.call(f.cap.chief, 'message.receive'), []); const second = await f.saveSpec('# Again\n'); assert.equal(f.notices().length, 4); assert.equal(f.notices().filter(n => n.body.includes(`commit ${second.commit}`)).length, 2); }); test('no notice for a docs save, an agent record commit, a git commit outside the Console, or a refused save', async t => { const f = await setup(t); const docs = await f.call(f.human, 'project.file', { project: 'stack', path: 'docs/README.md' }); await f.call(f.human, 'project.file.save', { project: 'stack', path: 'docs/README.md', base: docs.commit, content: '# Docs\n' }); await f.call(f.coder, 'record.write', { project: 'stack', path: 'docs/result.md', record: '# Result\n' }); const outside = f.save('# By git\n'); await reject(f.call(f.human, 'project.file.save', { project: 'stack', path: SPEC, base: f.commit, content: '# Stale\n' }), 'stale-base'); await reject(f.call(f.coder, 'project.file.save', { project: 'stack', path: SPEC, base: outside, content: '# Agent\n' }), 'human-required'); await reject(f.call(f.human, 'project.file.save', { project: 'stack', path: SPEC, base: 'abc', content: '# Bad\n' }), 'project-record-invalid'); assert.deepEqual(f.notices(), []); await f.saveSpec('# Mine\n'); assert.equal(f.notices().length, 2); }); test('a notice to a role no run holds waits for a claim, and launches nobody and tells nobody', async t => { const f = await setup(t, { running: ['chief', 'cto'] }); await f.saveSpec('# Edited\n'); const waiting = f.noticeTo('pm'); assert.ok(waiting); assert.equal(f.notices().length, 2); assert.deepEqual(await f.call(f.cap.chief, 'message.receive'), []); const pm = f.agent('pm'); assert.deepEqual((await f.call(pm, 'message.receive')).map(m => m.id), [waiting.id]); }); test('a business without a pm or a cto saves the spec and sends nothing', async t => { const f = await fixture(t); const open = await f.call(f.human, 'project.file', { project: 'stack', path: SPEC }); const saved = await f.call(f.human, 'project.file.save', { project: 'stack', path: SPEC, base: open.commit, content: '# Edited\n' }); assert.equal(git(f.repo, 'rev-parse', 'HEAD'), saved.commit); assert.deepEqual(f.store.all("SELECT 1 FROM messages WHERE from_role='_bus'"), []); }); test('a notice that cannot be stored answers outcome-unknown, and the commit stands', async t => { const f = await setup(t); f.store.run("CREATE TRIGGER no_notice BEFORE INSERT ON messages WHEN NEW.from_role='_bus' BEGIN SELECT RAISE(ABORT, 'no'); END"); const before = await f.head(); await reject(f.call(f.human, 'project.file.save', { project: 'stack', path: SPEC, base: before, content: '# Edited\n' }), 'outcome-unknown'); assert.notEqual(git(f.repo, 'rev-parse', 'HEAD'), before); assert.equal(f.notices().length, 0); }); test('a BLOCK reaches the chief of staff, the chief relays it to the assistant in the user\'s thread, and the commit stands', async t => { const f = await setup(t); const saved = await f.saveSpec('# Edited\n'); const block = f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: f.noticeTo('pm').id, body: 'Conflicts with decision 41.' }); assert.equal(f.threadOf(block.id), null); const [got] = await f.call(f.cap.chief, 'message.receive'); assert.equal(got.id, block.id); assert.equal(got.class, 'BLOCK'); assert.equal(got.from_role, 'pm'); const relay = f.send('chief', 'assistant', { class: 'INFO', in_reply_to: block.id, body: 'pm blocks the spec change.' }); assert.equal(f.threadOf(relay.id), f.threadId); assert.deepEqual((await f.call(f.cap.assistant, 'message.receive')).map(m => m.id), [relay.id]); assert.equal(git(f.repo, 'rev-parse', 'HEAD'), saved.commit); assert.equal(git(f.repo, 'show', `HEAD:${SPEC}`), '# Edited'); // The cto's accept is an ordinary INFO to the chief; it needs no BLOCK path. const accept = f.send('cto', 'chief', { class: 'INFO', in_reply_to: f.noticeTo('cto').id, body: 'Accept.' }); assert.equal(f.threadOf(accept.id), null); assert.equal(f.store.get('SELECT class FROM messages WHERE id=?', accept.id).class, 'INFO'); }); test('with no thread a BLOCK still reaches the chief of staff, and the relay to the assistant is refused', async t => { const f = await setup(t, { thread: false }); await f.saveSpec('# Edited\n'); const block = f.send('cto', 'chief', { class: 'BLOCK', in_reply_to: f.noticeTo('cto').id }); assert.deepEqual((await f.call(f.cap.chief, 'message.receive')).map(m => m.id), [block.id]); await refuse(() => f.send('chief', 'assistant', { class: 'INFO', in_reply_to: block.id }), 'thread-required-reply-to-thread-message'); }); test('within the notice\'s project the relay lands in the user\'s most recently opened thread', async t => { const f = await setup(t); const project = f.store.get('SELECT project FROM threads WHERE id=?', f.threadId).project; const beta = f.call(f.human, 'thread.create', { project, name: 'beta' }).id; await f.saveSpec('# Edited\n'); const block = f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: f.noticeTo('pm').id }); // No thread has been opened, so the newest created one wins; opening alpha moves the relay back to it. assert.equal(f.threadOf(f.send('chief', 'assistant', { class: 'INFO', in_reply_to: block.id }).id), beta); f.call(f.human, 'thread.open', { thread: f.threadId }); assert.equal(f.threadOf(f.send('chief', 'assistant', { class: 'INFO', in_reply_to: block.id }).id), f.threadId); }); test('a more recently opened thread in another project does not take the relay', async t => { const f = await setup(t); // The one assistant instance holds the user's threads in every project. The second project has no repo // (the broker clones the business at construction, so the fixture cannot add another), and it is the // thread the user opened last; the relay still belongs to the thread of the project the notice names. const other = f.call(f.human, 'project.create', { name: 'side notes' }).id; const gamma = f.call(f.human, 'thread.create', { project: other, name: 'gamma' }).id; f.call(f.human, 'thread.open', { thread: gamma }); assert.notEqual(gamma, f.threadId); await f.saveSpec('# Edited\n'); const block = f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: f.noticeTo('pm').id }); assert.equal(f.threadOf(f.send('chief', 'assistant', { class: 'INFO', in_reply_to: block.id }).id), f.threadId); }); test('a BLOCK is refused from anyone but a pm or cto, to anyone but the chief of staff, and outside a reply to its own notice', async t => { const f = await setup(t); await f.saveSpec('# Edited\n'); const mine = f.noticeTo('pm').id, theirs = f.noticeTo('cto').id; const plain = f.send('pm', 'chief', { class: 'INFO' }).id; const before = f.store.get('SELECT count(*) n FROM messages').n; await refuse(() => f.call(f.coder, 'message.send', { to: 'chief', body: 'x', class: 'BLOCK', in_reply_to: mine }), 'block-sender'); await refuse(() => f.call(f.human, 'message.send', { to: 'chief', body: 'x', class: 'BLOCK', in_reply_to: mine }), 'block-sender'); await refuse(() => f.call(f.cap.assistant, 'message.send', { to: 'chief', body: 'x', class: 'BLOCK', in_reply_to: mine }), 'block-sender'); await refuse(() => f.send('pm', 'cto', { class: 'BLOCK', in_reply_to: mine }), 'block-recipient'); await refuse(() => f.send('pm', 'human', { class: 'BLOCK', in_reply_to: mine }), 'block-recipient'); await refuse(() => f.send('pm', 'chief', { class: 'BLOCK' }), 'block-reply-required'); await refuse(() => f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: plain }), 'block-reply-required'); await refuse(() => f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: theirs }), 'block-reply-required'); // A bus notice of another kind (row 146's launch notice) is not a spec notice. const other = 'other-bus-notice'; f.store.run("INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,body) VALUES(?,?,?,?,?,?,?,?)", other, new Date().toISOString(), 'demo', '_bus', 'bus', 'pm', 'RESULT', 'A message for pm is waiting.'); const withOther = f.store.get('SELECT count(*) n FROM messages').n; await refuse(() => f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: other }), 'block-reply-required'); assert.equal(f.store.get('SELECT count(*) n FROM messages').n, withOther); assert.equal(withOther, before + 1); }); test('a role cannot forge a spec notice: its text opens neither a BLOCK nor the assistant\'s thread', async t => { const f = await setup(t); const real = (await (async () => { await f.saveSpec('# Edited\n'); return f.noticeTo('pm'); })()).body; const forged = f.send('chief', 'pm', { class: 'INFO', body: real }); await refuse(() => f.send('pm', 'chief', { class: 'BLOCK', in_reply_to: forged.id }), 'block-reply-required'); const answer = f.send('pm', 'chief', { class: 'INFO', in_reply_to: forged.id, body: 'Accept.' }); await refuse(() => f.send('chief', 'assistant', { class: 'INFO', in_reply_to: answer.id }), 'thread-required-reply-to-thread-message'); }); test('the bus receives nothing: a reply to a notice addressed to _bus is refused', async t => { const f = await setup(t); await f.saveSpec('# Edited\n'); await refuse(() => f.send('pm', '_bus', { in_reply_to: f.noticeTo('pm').id }), 'unknown-role'); });