// Row 90 (#1584): `requirements`, the approved PRD's ids that task.create takes, for the role // sessions that create tasks; and the PM's two no-task decisions, a request no id fits // (prd.approve) and a run it thinks should stop (role.revoke), which reach the human's Inbox. import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Store } from '../src/store.mjs'; import { Broker } from '../src/broker.mjs'; import { loadRequirements } from '../../tasks/src/requirements.mjs'; const options = [ { key: 'yes', text: 'Approve' }, { key: 'no', text: 'Decline' }, ]; const roles = { pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: [] } }, coder: { authority: { withinRole: ['message.send'], crossRole: [] } }, }; const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles } }; function setup(t) { const root = mkdtempSync(join(tmpdir(), 'bus-requirements-')); const store = new Store(root); const b = new Broker({ store, businesses }); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const agent = (role) => { const cap = b.bindLaunch({ business: 'demo', role, run: `demo-${role}-run`, harness: 'pi', address: role }); b.request(cap, { verb: 'role.claim' }); return cap; }; const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const reader = b.bindReader({ business: 'demo' }); return { b, agent, human, reader }; } const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args }); const raise = (b, cap, action, extra = {}) => call(b, cap, 'decision.raise', { action, question: `${action}?`, options, recommendation: 'no', blocking: false, ...extra }); test('requirements returns the committed list to a holding role session, and no one else', (t) => { const { b, agent, human, reader } = setup(t); const pm = agent('pm'); const list = call(b, pm, 'requirements'); assert.deepEqual(list, loadRequirements()); assert.equal(list.requirements.length, 24); assert.deepEqual(call(b, agent('coder'), 'requirements'), list); assert.throws(() => call(b, human, 'requirements'), /agent-required/); assert.throws(() => call(b, reader, 'requirements'), /read-only/); // A second launch of the role that never claimed it holds nothing. const stale = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm-other', harness: 'pi', address: 'pm2' }); assert.throws(() => call(b, stale, 'requirements'), /not-holder/); assert.throws(() => call(b, pm, 'requirements', { prd: 'docs/prd/mosaic-stack.md' }), /invalid-request/); }); test("the PM's prd.approve with no task, when no id fits, reaches the human's Inbox", (t) => { const { b, agent, human, reader } = setup(t); const pm = agent('pm'); const d = raise(b, pm, 'prd.approve', { target: 'mosaic-stack-prd' }); assert.deepEqual([d.class, d.route_to, d.blocking, d.task_ref], ['gated', 'human', false, null]); for (const cap of [human, reader]) assert.deepEqual(call(b, cap, 'inbox').map((r) => r.id), [d.id]); assert.deepEqual(call(b, pm, 'inbox'), []); // Blocking needs a task to hold. assert.throws(() => raise(b, pm, 'prd.approve', { blocking: true }), /task-required/); }); test("the PM's non-blocking role.revoke with no task reaches the human's Inbox", (t) => { const { b, agent, human } = setup(t); const pm = agent('pm'); agent('coder'); const d = raise(b, pm, 'role.revoke', { target: 'coder' }); assert.deepEqual([d.class, d.route_to, d.blocking, d.task_ref], ['gated', 'human', false, null]); assert.deepEqual(d.authorization, { action: 'role.revoke', target: 'coder', approvalChoice: 'yes' }); assert.deepEqual(call(b, human, 'inbox').map((r) => r.id), [d.id]); assert.throws(() => raise(b, pm, 'role.revoke', { target: 'coder', blocking: true }), /task-required/); });