Jason-approved merge, landed unchanged: git diff bbf61d6dd..3c80fea74 from work/vikunja-19-306. Business reviewer verdict 8ee5be8b approve on that commit; Jason resolved merge decision 0eebf83f via the CLI. Applied cleanly on the parent landing; no conflict. Filbert reviews the rebased diff. Integration with row 121, found in Filbert's round 1 (#1639, comment 28623): packages/bus/tests/csuite-messaging.test.mjs setup() binds the assistant to a thread, since row 121 made assistant a thread definition and bindLaunch refuses an assistant run without one. The human creates a project and thread first. No assertion or code changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
75 lines
4.5 KiB
JavaScript
75 lines
4.5 KiB
JavaScript
// vikunja:19/306 (REQ-ROLE-4, REQ-ASSIST-2): the five C-suite roles talk to the user, the chief of
|
|
// staff, the pm and the cto, and to no one else, so an advisory role can't send work to the coder or
|
|
// reviewer around the pm. The lists are read from the shipped role files, so the test pins the files
|
|
// the stack runs. A message outside the lists is refused and logged, as the assistant's are.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { 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';
|
|
|
|
const shipped = (name) => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
|
|
const CSUITE = ['chief-executive-officer', 'chief-financial-officer', 'chief-information-officer', 'chief-information-security-officer', 'chief-marketing-officer'];
|
|
const within = { authority: { withinRole: ['message.send'], crossRole: [] } };
|
|
const OTHERS = { chief: 'chief-of-staff', pm: 'pm', cto: 'cto', coder: 'coder', reviewer: 'reviewer', assistant: 'assistant' };
|
|
const roles = Object.fromEntries([...CSUITE.map((c) => [c, { definition: c, ...within }]), ...Object.entries(OTHERS).map(([i, d]) => [i, { definition: d, ...within }])]);
|
|
const definitions = Object.fromEntries([
|
|
...CSUITE.map((c) => [c, { messaging: shipped(c).messaging }]),
|
|
...Object.values(OTHERS).map((d) => [d, d === 'assistant' ? { messaging: shipped('assistant').messaging } : {}]),
|
|
]);
|
|
const RECIPIENT = 'message-recipient-refused';
|
|
const SENDER = 'message-sender-refused';
|
|
|
|
function setup(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'bus-csuite-msg-'));
|
|
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: 'cto', delivery: 'pm' }, roles, definitions, projects: { stack: { root: '/nonexistent' } } } } });
|
|
const caps = { human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
|
|
// Row 121: the assistant is a thread definition, so its run binds to a thread.
|
|
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' } }).id;
|
|
for (const role of Object.keys(roles)) {
|
|
caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi', ...(role === 'assistant' ? { thread } : {}) });
|
|
b.request(caps[role], { verb: 'role.claim' });
|
|
}
|
|
const send = (from, to) => b.request(caps[from], { verb: 'message.send', args: { to, body: 'hello' } });
|
|
const messages = () => store.get('SELECT count(*) n FROM messages').n;
|
|
const refusals = () => store.all("SELECT actor_role, body FROM events WHERE kind='action.refused' ORDER BY seq").map((e) => [e.actor_role, JSON.parse(e.body).code]);
|
|
return { send, messages, refusals };
|
|
}
|
|
|
|
test('each C-suite role file carries the same two lists: human, chief-of-staff, pm, cto', () => {
|
|
for (const c of CSUITE) assert.deepEqual(shipped(c).messaging, { sendTo: ['human', 'chief-of-staff', 'pm', 'cto'], receiveFrom: ['human', 'chief-of-staff', 'pm', 'cto'] }, c);
|
|
});
|
|
|
|
for (const c of CSUITE) {
|
|
test(`${c}: messages to and from the user, chief of staff, pm and cto pass`, (t) => {
|
|
const { send, messages } = setup(t);
|
|
for (const to of ['human', 'chief', 'pm', 'cto']) send(c, to);
|
|
for (const from of ['human', 'chief', 'pm', 'cto']) send(from, c);
|
|
assert.equal(messages(), 8);
|
|
});
|
|
|
|
test(`${c}: a message to or from the coder, reviewer or assistant is refused and logged, and nothing is stored`, (t) => {
|
|
const { send, messages, refusals } = setup(t);
|
|
const out = ['coder', 'reviewer', 'assistant'];
|
|
for (const to of out) assert.throws(() => send(c, to), { code: RECIPIENT }, `to ${to}`);
|
|
for (const from of out) assert.throws(() => send(from, c), from === 'assistant' ? { code: RECIPIENT } : { code: SENDER }, `from ${from}`);
|
|
assert.equal(messages(), 0);
|
|
assert.deepEqual(refusals(), [
|
|
...out.map(() => [c, RECIPIENT]),
|
|
['coder', SENDER], ['reviewer', SENDER], ['assistant', RECIPIENT],
|
|
]);
|
|
});
|
|
|
|
test(`${c}: another C-suite role isn't on the lists either`, (t) => {
|
|
const { send } = setup(t);
|
|
const other = CSUITE.find((x) => x !== c);
|
|
assert.throws(() => send(c, other), { code: RECIPIENT });
|
|
});
|
|
}
|