Files
stack/packages/bus/tests/spec-review.test.mjs

191 lines
13 KiB
JavaScript

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');
});