274 lines
14 KiB
JavaScript
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);
|
|
});
|