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