Files
stack/packages/bus/tests/requirements.test.mjs
T
jason.woltjeandClaude Opus 5.5 f7c8537b32 prd: version 1.1, the Organization requirements (decision 03130d08, task 19/265)
Squash of the PM's work/vikunja-19-265 branch, net diff 0c3826910..8ae5a1491
(patch sha256 47b34e6054a74eedd8a5b6577e40537a07b2fb8fe5846f309e100146238ef743).
The PRD moves to version 1.1. "Business" becomes "organization" in
REQ-ROLE-3, REQ-ROLE-4 and REQ-LAUNCH-1. REQ-DEC-3 and REQ-DEC-4 name an
authorized human instead of Jason. Non-goal D adds that v1 has no hosted
SaaS, and "Where it runs" says Mosaic Stack stays self-hosted open source.
A new Organizations section adds REQ-ORG-1, REQ-ORG-2, REQ-USER-1,
REQ-THREAD-1, REQ-TPL-1 and REQ-FED-1. packages/tasks/requirements.json
matches at version 1.1 with 30 ids, and the tasks and bus requirements
tests expect 30.

The branch's 010d30cbe (parseRequirementId, task 19/229) is reverted on
the branch and is not part of this squash.

Jason approved the PRD in decision 03130d08 and the merge in decision
f71c73c9 (resolved yes via the CLI, 2026-10-14T16:35:32Z). The stack
reviewer's verdict d3f97f9d approves 8ae5a1491. Sage's gate on 1c9a4910d
plus the patch passed: every package suite and every scripts/test-*.sh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:49:36 -05:00

79 lines
3.9 KiB
JavaScript

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