86 lines
5.5 KiB
JavaScript
86 lines
5.5 KiB
JavaScript
// Darkwing review probes. Not part of the candidate.
|
|
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 { fork } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { Store } from '../src/store.mjs';
|
|
import { Broker } from '../src/broker.mjs';
|
|
const options = [{ key: 'yes', text: 'Allow' }, { key: 'no', text: 'Decline' }];
|
|
// Data-model table: cto crossRole task.scope.change, pm crossRole task.priority.change.
|
|
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles: {
|
|
pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: ['task.priority.change'] } },
|
|
cto: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } },
|
|
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } } } } };
|
|
function setup(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'dw-bus-'));
|
|
const store = new Store(root);
|
|
const b = new Broker({ store, businesses });
|
|
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
|
|
const agent = (role, run = role + '-run') => b.bindLaunch({ business: 'demo', role, run, harness: 'pi', address: run });
|
|
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
|
return { b, store, agent, human };
|
|
}
|
|
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
|
|
const raise = (b, cap, action, extra = {}) => call(b, cap, 'decision.raise', { action, question: 'Allow?', options, recommendation: 'no', blocking: false, ...extra });
|
|
|
|
test('P1 revoked run reclaims its role with its old capability', (t) => {
|
|
const { b, agent, human } = setup(t), c = agent('coder'), p = agent('pm');
|
|
call(b, c, 'role.claim'); call(b, p, 'role.claim');
|
|
const d = raise(b, c, 'role.revoke', { target: 'pm' });
|
|
call(b, human, 'decision.resolve', { id: d.id, choice: 'yes' });
|
|
call(b, c, 'role.revoke', { role: 'pm', decision: d.id });
|
|
let out; try { out = call(b, p, 'role.claim'); } catch (e) { out = e.code; }
|
|
console.log('P1 revoked pm reclaims ->', JSON.stringify(out));
|
|
const p2 = agent('pm', 'pm-next');
|
|
let out2; try { out2 = call(b, p2, 'role.claim'); } catch (e) { out2 = e.code; }
|
|
console.log('P1 replacement pm claim ->', JSON.stringify(out2));
|
|
});
|
|
test('P2 arbiter resolves its own cross-role decision', (t) => {
|
|
const { b, agent } = setup(t), cto = agent('cto'), pm = agent('pm');
|
|
call(b, cto, 'role.claim'); call(b, pm, 'role.claim');
|
|
const d = raise(b, cto, 'task.scope.change', { domain: 'technical', target: 'x1' });
|
|
console.log('P2 cto raise route_to', d.route_to);
|
|
call(b, cto, 'decision.resolve', { id: d.id, choice: 'yes' });
|
|
console.log('P2 cto authorize ->', JSON.stringify(b.authorize(cto, 'task.scope.change', { decision: d.id, target: 'x1' })));
|
|
const d2 = raise(b, pm, 'task.priority.change', { target: 'x2' });
|
|
console.log('P2 pm raise route_to', d2.route_to);
|
|
call(b, pm, 'decision.resolve', { id: d2.id, choice: 'yes' });
|
|
console.log('P2 pm authorize ->', JSON.stringify(b.authorize(pm, 'task.priority.change', { decision: d2.id, target: 'x2' })));
|
|
});
|
|
test('P3 one approval authorizes repeatedly', (t) => {
|
|
const { b, agent, human } = setup(t), c = agent('coder');
|
|
call(b, c, 'role.claim');
|
|
const d = raise(b, c, 'deploy', { target: 'v1' });
|
|
call(b, human, 'decision.resolve', { id: d.id, choice: 'yes' });
|
|
for (let i = 0; i < 3; i++) console.log('P3 authorize', i, JSON.stringify(b.authorize(c, 'deploy', { decision: d.id, target: 'v1' })));
|
|
});
|
|
test('P4 refusals from a reader and a non-holder append events', (t) => {
|
|
const { b, store, agent } = setup(t), r = b.bindReader({ business: 'demo' }), c = agent('coder');
|
|
const before = store.get('SELECT count(*) n FROM events').n;
|
|
for (let i = 0; i < 5; i++) { try { call(b, r, 'role.claim'); } catch {} try { call(b, c, 'message.send', { to: 'pm', body: 'x' }); } catch {} }
|
|
console.log('P4 events added by 10 refusals:', store.get('SELECT count(*) n FROM events').n - before);
|
|
});
|
|
test('P5 empty in_reply_to', (t) => {
|
|
const { b, store, agent } = setup(t), c = agent('coder');
|
|
call(b, c, 'role.claim');
|
|
let out; try { out = call(b, c, 'message.send', { to: 'pm', body: 'x', in_reply_to: '' }); out = store.get('SELECT in_reply_to FROM messages WHERE id=?', out.id); } catch (e) { out = e.code; }
|
|
console.log('P5 ->', JSON.stringify(out));
|
|
});
|
|
test('P6 a refused bindLaunch over IPC stops the broker process', async (t) => {
|
|
const root = mkdtempSync(join(tmpdir(), 'dw-bus-p6-'));
|
|
const child = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] });
|
|
t.after(() => { if (child.exitCode === null) child.kill('SIGKILL'); rmSync(root, { recursive: true, force: true }); });
|
|
let m = once(child, 'message');
|
|
child.send({ op: 'boot', config: { dataRoot: root, businesses, launches: [] } });
|
|
console.log('P6 boot', (await m)[0].ok);
|
|
const rec = { business: 'demo', role: 'cto', run: 'r1', harness: 'pi', pid: process.pid, startTime: '1' };
|
|
m = once(child, 'message'); child.send({ op: 'bindLaunch', record: rec }); console.log('P6 first bind', (await m)[0].ok);
|
|
const exit = once(child, 'exit');
|
|
m = once(child, 'message'); child.send({ op: 'bindLaunch', record: rec }); console.log('P6 duplicate bind', JSON.stringify((await m)[0]));
|
|
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
|
|
console.log('P6 exit code after refused bind', (await exit)[0]); clearTimeout(timer);
|
|
});
|