191 lines
13 KiB
JavaScript
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');
|
|
});
|