Files
stack/packages/bus/tests/resolve-rights.test.mjs

83 lines
4.8 KiB
JavaScript

// RR (#1873): resolution checks current role rights, and callers cannot choose a merge/policy arbiter.
import test from 'node:test';
import assert from 'node:assert/strict';
import { loadRole } from '../../organization/src/index.mjs';
import { REPO_ROLES } from '../../organization/tests/helpers.mjs';
import { fixture } from './spec-fixtures.mjs';
import { grantDecision } from './host-fixtures.mjs';
async function setup(t, { shared = false } = {}) {
const roles = Object.fromEntries(['cto', 'pm', 'chief-of-staff'].map(definition => {
const role = loadRole(REPO_ROLES, definition);
return [definition === 'chief-of-staff' ? 'chief' : definition, { definition, authority: structuredClone(role.authority) }];
}));
roles.coder = { definition: 'coder', authority: { withinRole: ['message.send', 'record.write'], crossRole: ['task.scope.change', 'git.merge.protected', 'policy.change'] } };
const f = await fixture(t, { roles, arbiters: { technical: 'cto', delivery: shared ? 'cto' : 'pm' } });
return { ...f, roles, cto: f.agent('cto'), pm: f.agent('pm'), chief: f.chief };
}
for (const [domain, resolver] of [['technical', 'cto'], ['delivery', 'pm']]) {
test(`RR: ${domain} arbiter that loses its right refuses not-resolver`, async t => {
const f = await setup(t);
const d = f.raise({ action: 'task.scope.change', domain });
f.business.roles[resolver].authority.withinRole = ['message.send'];
f.broker.reloadBusiness('demo', f.business);
assert.throws(() => f.call(f[resolver], 'decision.resolve', { id: d.id, choice: 'yes' }), error => {
assert.equal(error.code, 'not-resolver');
assert.match(error.reason, new RegExp(`${resolver}.*${resolver}.*decision\\.resolve\\.${domain}`));
return true;
});
assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?', d.id).n, 0);
});
}
test('RR: a shared arbiter cannot resolve a delivery decision using only its technical right', async t => {
const f = await setup(t, { shared: true });
const d = f.raise({ action: 'task.scope.change', domain: 'delivery' });
assert.equal(d.route_to, 'cto');
assert.throws(() => f.call(f.cto, 'decision.resolve', { id: d.id, choice: 'yes' }), { code: 'not-resolver' });
});
test('RR: merge raised with delivery still routes to CTO and resolves (acceptance 314 step 5)', async t => {
const f = await setup(t);
const d = await f.raise({ action: 'git.merge.protected', domain: 'delivery', project: 'stack', spec_path: 'spec/north-star.md', spec_commit: f.commit });
assert.equal(d.route_to, 'cto');
assert.throws(() => f.call(f.pm, 'decision.resolve', { id: d.id, choice: 'yes' }), { code: 'not-resolver' });
f.call(f.cto, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.equal(f.store.get('SELECT op FROM decision_events WHERE decision=?', d.id).op, 'resolved');
});
for (const resolver of ['chief', 'pm']) {
test(`RR: ${resolver} named delivery arbiter resolves policy.change even when raised technical`, async t => {
const f = await setup(t); f.business.arbiters.delivery = resolver;
f.broker.reloadBusiness('demo', f.business);
const d = f.raise({ action: 'policy.change', domain: 'technical' });
assert.equal(d.route_to, resolver);
f.call(f[resolver], 'decision.resolve', { id: d.id, choice: 'yes' });
assert.equal(f.store.get('SELECT by FROM decision_events WHERE decision=?', d.id).by, `${resolver}-run`);
});
}
test('RR: the chief holds both rights when named for both domains', async t => {
const f = await setup(t); f.business.arbiters = { technical: 'chief', delivery: 'chief' };
f.broker.reloadBusiness('demo', f.business);
for (const action of ['git.merge.protected', 'policy.change']) {
const d = f.raise({ action }); assert.equal(d.route_to, 'chief');
f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' });
}
});
// Old raise evidence did not preserve domain. Current assignments cannot prove the old domain.
test('RR: remapping arbiters cannot let a legacy decision resolve with only the other domain right', async t => {
const f = await setup(t);
const id = grantDecision(f.store, { action: 'task.scope.change', route: 'pm', domain: null, close: null });
f.business.arbiters = { technical: 'pm', delivery: 'cto' };
f.business.roles.pm.authority.withinRole = ['message.send', 'decision.resolve.technical'];
f.broker.reloadBusiness('demo', f.business);
assert.throws(() => f.call(f.pm, 'decision.resolve', { id, choice: 'yes' }), { code: 'not-resolver' });
assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?', id).n, 0);
// Human recovery closes the old route by withdrawing it, before the raiser creates a new decision.
f.call(f.human, 'decision.withdraw', { id });
assert.equal(f.store.get('SELECT op FROM decision_events WHERE decision=?', id).op, 'withdrawn');
});