Files
stack/packages/bus/tests/staff-request.test.mjs

274 lines
14 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, mkdtempSync, 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';
// Row 140 (#1634, REQ-COS-1): the pm asks the chief of staff for role sessions with staff.request, and
// the chief of staff answers by message. The grant is message.send, which the pm already holds, so
// the request widens nothing; the roles' authority is read from the shipped files.
const shipped = (name) =>
JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
const authority = { pm: shipped('pm').authority, chief: shipped('chief-of-staff').authority };
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
const TASK = 'vikunja:1/2';
const WANT = { task_ref: TASK, roles: [{ role: 'coder', count: 2 }, { role: 'reviewer', count: 1 }] };
function setup(t, { launch, definitions = {}, roles, arbiters = { technical: 'pm', delivery: 'chief' } } = {}) {
const root = mkdtempSync(join(tmpdir(), 'bus-staff-'));
const store = new Store(root);
t.after(() => {
store.close();
rmSync(root, { recursive: true, force: true });
});
const set = roles ?? {
chief: { definition: 'chief-of-staff', authority: authority.chief },
pm: { definition: 'pm', authority: authority.pm },
coder: { definition: 'coder', ...none },
assistant: { definition: 'assistant', ...none },
};
const b = new Broker({
store,
businesses: {
demo: {
id: 'demo',
human: 'jason',
arbiters,
launch: launch === undefined ? { by: 'chief', instances: ['coder'], max: { opus: 2, sonnet: 2 } } : launch,
roles: set,
definitions,
vars: { 'bus.tester': true },
},
},
});
const caps = {
human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }),
reader: b.bindReader({ business: 'demo' }),
tester: b.bindTester({ business: 'demo' }),
};
// The assistant runs for one thread (row 121), so it is never launched here; thread.send needs only the thread.
let thread;
if (set.assistant) {
const project = b.request(caps.human, { verb: 'project.create', args: { name: 'demo' } }).id;
thread = b.request(caps.human, { verb: 'thread.create', args: { project, name: 'main' } }).id;
}
for (const role of Object.keys(set).filter((r) => r !== 'assistant')) {
caps[role] = b.bindLaunch({ business: 'demo', role, run: `${role}-run`, harness: 'pi' });
b.request(caps[role], { verb: 'role.claim' });
}
const call = (who, verb, args = {}) => b.request(caps[who], { verb, args });
const allowed = () =>
store
.all("SELECT actor_role, subject, body FROM events WHERE kind='action.allowed' ORDER BY seq")
.map((e) => ({ ...e, body: JSON.parse(e.body) }));
const staffMessages = () => store.all("SELECT * FROM messages WHERE class='STAFF-REQUEST'");
const staffEvents = () => allowed().filter((e) => e.body.operation === 'staff.request');
return { b, store, caps, call, allowed, staffEvents, staffMessages, thread };
}
test('REQ-COS-1: the pm asks the chief of staff for roles and counts; the chief receives a STAFF-REQUEST and answers it', (t) => {
const { call, allowed } = setup(t);
const sent = call('pm', 'staff.request', { ...WANT, note: 'the build needs two coders' });
assert.deepEqual(sent, { message: sent.message, to: 'chief' });
const [m, ...rest] = call('chief', 'message.receive');
assert.equal(rest.length, 0);
assert.equal(m.id, sent.message);
assert.equal(m.class, 'STAFF-REQUEST');
assert.equal(m.from_role, 'pm');
assert.equal(m.to_role, 'chief');
assert.match(m.body, /^Staff request for vikunja:1\/2: coder x2, reviewer x1\. /);
assert.match(m.body, /\n\nthe build needs two coders$/);
// The chief of staff answers in reply, with the instances it launched or a refusal and its reason.
const answer = call('chief', 'message.send', { to: 'pm', body: 'launched coder; reviewer refused: capacity-full', class: 'RESULT', in_reply_to: m.id });
const [back] = call('pm', 'message.receive');
assert.equal(back.id, answer.id);
assert.equal(back.in_reply_to, m.id);
// It spends the pm's message.send, within the role, and the log names the request.
const [event, ...more] = allowed().filter((e) => e.body.operation === 'staff.request');
assert.equal(more.length, 0);
assert.equal(event.actor_role, 'pm');
assert.equal(event.body.action, 'message.send');
assert.equal(event.body.class, 'within-role');
assert.equal(event.body.target, 'chief');
assert.equal(event.body.staff_for, TASK);
assert.deepEqual(event.body.roles, WANT.roles);
});
test('REQ-COS-1: only the pm can send a staff request; every other session is refused and the chief of staff hears nothing', (t) => {
const { call, staffMessages, thread } = setup(t);
for (const who of ['coder', 'chief']) {
assert.throws(() => call(who, 'staff.request', WANT), /staff-request-refused/, who);
// message.send can't write the class either, from any role.
assert.throws(() => call(who, 'message.send', { to: 'chief', body: 'x', class: 'STAFF-REQUEST' }), /invalid-class/, who);
}
assert.throws(() => call('pm', 'message.send', { to: 'chief', body: 'x', class: 'STAFF-REQUEST' }), /invalid-class/);
assert.throws(() => call('human', 'staff.request', WANT), /agent-required/);
assert.throws(() => call('human', 'message.send', { to: 'chief', body: 'x', class: 'STAFF-REQUEST' }), /invalid-class/);
assert.throws(() => call('human', 'thread.send', { thread, body: 'x', class: 'STAFF-REQUEST' }), /invalid-class/);
assert.throws(() => call('reader', 'staff.request', WANT), /read-only/);
assert.throws(() => call('tester', 'staff.request', WANT), /tester-refused/);
assert.deepEqual(call('chief', 'message.receive'), []);
assert.deepEqual(staffMessages(), []);
});
test('REQ-COS-1: a pm without message.send is refused like any other gated action', (t) => {
const { call, staffMessages } = setup(t, {
roles: {
chief: { definition: 'chief-of-staff', authority: authority.chief },
pm: { definition: 'pm', authority: { withinRole: ['task.assign'], crossRole: [] } },
},
});
assert.throws(() => call('pm', 'staff.request', WANT), /decision-required/);
assert.deepEqual(staffMessages(), []);
});
test('REQ-COS-1: the request goes to the business launcher, and only when that is a chief of staff', (t) => {
for (const launch of [null, { by: 'coder', instances: ['pm'], max: { opus: 1, sonnet: 1 } }]) {
const { call, staffMessages } = setup(t, { launch });
assert.throws(() => call('pm', 'staff.request', WANT), /no-chief-of-staff/, JSON.stringify(launch));
assert.deepEqual(staffMessages(), []);
}
});
// Round 2 (Filbert, #1634 comment 28639): the sender and the launcher are judged by their role
// definition, never by the instance name. Every fixture above names the pm instance `pm` and the
// launcher `chief`, so a check on the name passes there; these use names that differ from the definitions.
test('REQ-COS-1: the sender is judged by definition: an instance `lead` of the pm definition can ask, an instance `pm` of the coder definition can\'t', (t) => {
const lead = setup(t, {
arbiters: { technical: 'chief', delivery: 'chief' },
roles: {
chief: { definition: 'chief-of-staff', authority: authority.chief },
lead: { definition: 'pm', authority: authority.pm },
coder: { definition: 'coder', ...none },
},
});
const sent = lead.call('lead', 'staff.request', WANT);
assert.deepEqual(sent, { message: sent.message, to: 'chief' });
const [m] = lead.call('chief', 'message.receive');
assert.equal(m.id, sent.message);
assert.equal(m.from_role, 'lead');
assert.equal(lead.staffEvents().length, 1);
assert.equal(lead.staffEvents()[0].actor_role, 'lead');
// The coder instance is still refused, and the instance named `pm` below is not the pm definition.
assert.throws(() => lead.call('coder', 'staff.request', WANT), /staff-request-refused/);
const named = setup(t, {
roles: {
chief: { definition: 'chief-of-staff', authority: authority.chief },
pm: { definition: 'coder', authority: authority.pm },
coder: { definition: 'coder', ...none },
},
});
assert.throws(() => named.call('pm', 'staff.request', WANT), /staff-request-refused/);
assert.deepEqual(named.call('chief', 'message.receive'), []);
assert.deepEqual(named.staffMessages(), []);
// The refusals leave no staff.request action.allowed event: the authority spend rolls back with them.
assert.deepEqual(named.staffEvents(), []);
assert.equal(lead.staffEvents().length, 1);
});
test('REQ-COS-1: the launcher is judged by definition: an instance `boss` of the chief-of-staff definition receives, a `chief` of the coder definition gives no-chief-of-staff', (t) => {
const boss = setup(t, {
arbiters: { technical: 'pm', delivery: 'chief' },
launch: { by: 'boss', instances: ['coder'], max: { opus: 2, sonnet: 2 } },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
boss: { definition: 'chief-of-staff', authority: authority.chief },
pm: { definition: 'pm', authority: authority.pm },
coder: { definition: 'coder', ...none },
},
});
const sent = boss.call('pm', 'staff.request', WANT);
assert.deepEqual(sent, { message: sent.message, to: 'boss' });
const [m] = boss.call('boss', 'message.receive');
assert.equal(m.id, sent.message);
assert.equal(m.to_role, 'boss');
assert.equal(m.class, 'STAFF-REQUEST');
const wrong = setup(t, {
roles: {
chief: { definition: 'coder', authority: authority.chief },
pm: { definition: 'pm', authority: authority.pm },
coder: { definition: 'coder', ...none },
},
});
assert.throws(() => wrong.call('pm', 'staff.request', WANT), /no-chief-of-staff/);
assert.deepEqual(wrong.call('chief', 'message.receive'), []);
assert.deepEqual(wrong.staffMessages(), []);
assert.deepEqual(wrong.staffEvents(), []);
});
test('REQ-COS-1: a refused staff request leaves no staff.request action.allowed event', (t) => {
const { call, staffEvents, staffMessages } = setup(t, { launch: null });
assert.throws(() => call('pm', 'staff.request', WANT), /no-chief-of-staff/);
assert.throws(() => call('coder', 'staff.request', WANT), /staff-request-refused/);
assert.throws(() => call('pm', 'staff.request', { ...WANT, roles: [] }), /invalid-request/);
assert.deepEqual(staffEvents(), []);
assert.deepEqual(staffMessages(), []);
});
test('REQ-ASSIST-2: the role definitions\' messaging lists hold for a staff request, sender and recipient', (t) => {
const out = setup(t, { definitions: { pm: { messaging: { sendTo: ['assistant'], receiveFrom: ['chief-of-staff'] } } } });
assert.throws(() => out.call('pm', 'staff.request', WANT), /message-recipient-refused/);
const into = setup(t, { definitions: { 'chief-of-staff': { messaging: { sendTo: ['pm'], receiveFrom: ['assistant'] } } } });
assert.throws(() => into.call('pm', 'staff.request', WANT), /message-sender-refused/);
const open = setup(t, { definitions: { 'chief-of-staff': { messaging: { sendTo: ['pm'], receiveFrom: ['pm'] } } } });
assert.equal(open.call('pm', 'staff.request', WANT).to, 'chief');
for (const s of [out, into]) assert.deepEqual(s.staffMessages(), []);
});
test('a staff request names a task, 1 to 10 distinct roles and counts of 1 to 20; nothing else is accepted', (t) => {
const { call, staffMessages } = setup(t);
const roles = (n) => Array.from({ length: n }, (_, i) => ({ role: `role-${i}`, count: 1 }));
const bad = [
{},
{ roles: WANT.roles },
{ task_ref: TASK },
{ ...WANT, task_ref: 'vikunja:0/2' },
{ ...WANT, task_ref: 'T-1' },
{ ...WANT, roles: [] },
{ ...WANT, roles: 'coder' },
{ ...WANT, roles: roles(11) },
{ ...WANT, roles: [{ role: 'coder', count: 0 }] },
{ ...WANT, roles: [{ role: 'coder', count: 21 }] },
{ ...WANT, roles: [{ role: 'coder', count: 1.5 }] },
{ ...WANT, roles: [{ role: 'coder', count: '2' }] },
{ ...WANT, roles: [{ role: 'coder' }] },
{ ...WANT, roles: [{ count: 1 }] },
{ ...WANT, roles: [{ role: 'coder', count: 1, model: 'opus' }] },
{ ...WANT, roles: [{ role: 'bad role', count: 1 }] },
{ ...WANT, roles: [{ role: 'coder', count: 1 }, { role: 'coder', count: 2 }] },
{ ...WANT, note: '' },
{ ...WANT, to: 'coder' },
];
for (const args of bad) assert.throws(() => call('pm', 'staff.request', args), /invalid-request/, JSON.stringify(args));
assert.deepEqual(staffMessages(), []);
for (const ok of [roles(10), [{ role: 'coder', count: 20 }], [{ role: 'coder', count: 1 }]])
assert.equal(call('pm', 'staff.request', { task_ref: TASK, roles: ok }).to, 'chief');
assert.equal(staffMessages().length, 3);
});
test('REQ-COS-1: a staff request grants nothing: the pm still can\'t launch and the chief of staff launches as before', (t) => {
// The shipped files: the pm holds no role.launch, the chief of staff holds it within its role.
assert.ok(!authority.pm.withinRole.includes('role.launch') && !authority.pm.crossRole.includes('role.launch'));
assert.ok(authority.chief.withinRole.includes('role.launch'));
const { b, caps, call } = setup(t);
const launches = () => ({
pm: (() => {
try {
return b.authorize(caps.pm, 'role.launch', { target: 'coder' }).class;
} catch (e) {
return e.code ?? e.message;
}
})(),
chief: b.authorize(caps.chief, 'role.launch', { target: 'coder' }).class,
});
const before = launches();
assert.match(before.pm, /decision-required/);
assert.equal(before.chief, 'within-role');
call('pm', 'staff.request', WANT);
call('chief', 'message.receive');
assert.deepEqual(launches(), before);
});