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