Files
stack/packages/bus/tests/autonomy-authority.test.mjs
jason.woltje 23e504f331 fix(cli,bus,organization): E2E6 lane f6c, refusal lines name the next step (#1873)
1. Stale bus host lock refusals from bus status, start, stop and restart
   name mosaic bus recover ORG.
2. org definitions: unknown org, unknown or malformed name and bad id give
   one refusal or usage line, no stack trace, no host path.
3. Pack, board and workflow refusals print their hint as template save does;
   tested with the real CliError transport shape.
4. console start and stop clear a console/start.lock whose named supervisor
   (pid and start time) is gone; a live or unnamed holder refuses.
5. Template save keeps free text verbatim and writes no placeholders; the
   preview lists each field that mentions an org-specific name.
6. A request the shim never sent (no socket, connect refused) exits 5 and
   the CLI reports bus-not-running with the start and recover line.
7. autonomy-precondition carries the check that failed, including on the
   hire paths.
8. Saving under an existing template name refuses template-exists with a
   next step; refusal table entry added, channel-proof pins moved.
2026-11-13 20:14:21 -06:00

142 lines
13 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,mkdirSync,readFileSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {Broker} from '../src/broker.mjs';
import {Store} from '../src/store.mjs';
import {autonomyHost,authorityHost} from '../src/autonomy-host.mjs';
import {organizationWriter} from '../../organization/src/organization.mjs';
import {instanceSettings} from '../../organization/src/instance-settings.mjs';
import {hireDecisions} from '../../organization/src/hire-decisions.mjs';
import {loadBusiness} from '../../organization/src/organization-config.mjs';
import {businessDoc,rolesCopy,writeJson} from '../../organization/tests/helpers.mjs';
const inline={tools:['read'],network:'none',authority:{withinRole:[],crossRole:[]},credentials:[],instructions:'Changed role'};
const cap={limit:100000,since:'2026-01-01T00:00:00.000Z'};
function world(t,options={}) {
const root=mkdtempSync(join(tmpdir(),'au-authority-')),rolesDir=rolesCopy(root),dataRoot=join(root,'data');mkdirSync(dataRoot,{mode:0o700});
const doc=businessDoc(root,'demo');doc.plugins={harness:['pi'],'limit-check':['token-limit']};
doc.launch.limits.tokens={providers:{zai:cap}};
// #1873: an account quota alone, covered only through the host's declared harness fact.
if(options.account){doc.launch.account={name:'shared',credential:'FIXTURE_TOKEN'};doc.launch.limits.tokens={keys:{shared:cap}};}
doc.definitions={judge:{rev:1,role:{...inline,authority:{withinRole:['decision.resolve.authority'],crossRole:[]}},instructions:'Judge'},scout:{rev:3,role:structuredClone(inline),instructions:'Prior role'}};
// Definition instructions live beside the role composition.
for(const d of Object.values(doc.definitions))delete d.role.instructions;
doc.roles.judge={definition:'org:judge',vars:{harness:'pi',model:'claude-opus-5-5'}};
doc.roles.scout={definition:'org:scout',vars:{harness:'pi',model:'claude-opus-5-5'}};
doc.launch.instances.push('scout');doc.autonomy={authority:'near the loop',authorityResolver:'judge',notices:true,reverseWindowHours:1};
writeJson(join(root,'businesses/demo.json'),doc);
let reversing=false,broker,now=Date.now;
const writer=organizationWriter({dir:root,rolesDir,barrier:point=>{if(reversing && point==='prepared' && options.expirePrepared)Date.now=()=>now()+3600001;if(!reversing && point==='prepared' && options.removeResolverPrepared){const b=structuredClone(loaded());b.roles.judge.authority.withinRole=[];broker.reloadBusiness('demo',b);}}});
const store=new Store(dataRoot),order=[];
const loaded=()=>{const b=loadBusiness('demo',{dir:root,rolesDir});return {...b,roles:Object.fromEntries(Object.entries(b.roles).map(([n,e])=>[n,{...b.definitions[e.definition],...e}]))};};
const reload=id=>{if(reversing && options.crossAfterCommit)Date.now=()=>now()+3600001;if(reversing && options.reloadRefused)return false;broker.reloadBusiness(id,loaded());return true;};
const service=hireDecisions({writer,rolesDir,configDir:root,dataRoot,assertClean:()=>{}});
const hires=authorityHost(service,{reload,modelKeys:{prepareRevoke:async(instance,guard)=>{guard();assert.equal(instance,'demo/scout');order.push('prepare');},completeRevoke:instance=>{assert.equal(instance,'demo/scout');assert.ok(store.get("SELECT 1 FROM events WHERE json_extract(body,'$.action')='fire.stopped'"));assert.equal(writer.read('demo').document.definitions.scout.instructions,'Changed role');order.push('complete');}}});
const host=autonomyHost({writer,dataRoot,business:()=>loaded(),hires,reload,...(options.countsAccount?{countsAccount:options.countsAccount}:{})});
const instances=instanceSettings({dir:root,rolesDir,writer,revoke:()=>{order.push('fire-revoke');}});
broker=new Broker({store,businesses:{demo:loaded()},hires,autonomy:host,organization:(id,verb,args,actor,context)=>{if(verb==='instance.fireCheck'){instances.checkFire(id,args.name);return {};}if(verb==='instance.fire')return instances.fire(id,args,{actor,context});throw Error('unexpected organization verb');}});
const caps={};const call=(who,verb,args={})=>broker.request(caps[who],{verb,args});
for(const role of ['chief','judge','scout']){caps[role]=broker.bindLaunch({business:'demo',role,run:role+'-run',harness:'pi'});call(role,'role.claim');}
caps.owner=broker.bindConsole({business:'demo',human:'jason',subject:{provider:'test',iss:'test',sub:'owner'}});
t.after(()=>{Date.now=now;store.close();rmSync(root,{recursive:true,force:true});});
const raise=(extra={})=>call('chief','hire.raise',{op:'change',level:'org',name:'scout',role:{inline},route:{harness:'pi',model:'claude-opus-5-5'},reason:'Change the task',...extra});
const reverse=id=>{reversing=true;return call('owner','decision.reverse.console',{id});};
return {writer,store,order,broker,call,raise,reverse,loaded};
}
for(const crossAfterCommit of [false,true])test(`AU authority reverse uses runtime key wrapper and relaunches after commit (deadline crossed: ${crossAfterCommit})`,async t=>{
const f=world(t,{crossAfterCommit}),d=await f.raise();assert.deepEqual(d.hire.flagged,[]);assert.equal(d.route_to,'judge');
await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});assert.equal(f.writer.read('demo').document.definitions.scout.rev,4);
const r=await f.reverse(d.id);assert.equal(r.committed,true);assert.equal(r.relaunch,'requested');assert.deepEqual(f.order,['prepare','complete']);
const restored=f.writer.read('demo').document.definitions.scout;assert.equal(restored.rev,5);assert.equal(restored.instructions,'Prior role');
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse'").n,1);
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse.failed'").n,0);
assert.ok(f.store.get("SELECT 1 FROM events WHERE json_extract(body,'$.action')='lifecycle.launch' AND subject='scout'"));
});
test('AU a committed authority reverse reports pending recovery when reload refuses',async t=>{
const f=world(t,{reloadRefused:true}),d=await f.raise();await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});
const r=await f.reverse(d.id);assert.equal(r.committed,true);assert.equal(r.restart,true);assert.equal(r.relaunch,'pending');assert.equal(f.writer.read('demo').document.definitions.scout.instructions,'Prior role');
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse.failed'").n,0);
});
test('AU authority reverse crossing deadline in prepared IO never publishes',async t=>{
const f=world(t,{expirePrepared:true}),d=await f.raise();await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});const prior=f.writer.read('demo').revision;
await assert.rejects(f.reverse(d.id),{code:'reverse-window-closed'});assert.equal(f.writer.read('demo').revision,prior);f.writer.recover('demo');assert.equal(f.writer.read('demo').revision,prior);
});
test('AU authority resolver rechecks budget removal after a proposal was raised',async t=>{
const f=world(t),d=await f.raise(),b=structuredClone(f.loaded());b.plugins['limit-check']=[];f.broker.reloadBusiness('demo',b);
const before=f.writer.read('demo').revision;await assert.rejects(f.call('judge','decision.resolve',{id:d.id,choice:'yes'}),{code:'autonomy-precondition',reason:'no enforced budget covers new agents; authority away from the loop needs one'});
assert.equal(f.writer.read('demo').revision,before);assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?',d.id).n,0);
});
test('AU authority apply rechecks a removed resolve right at publication',async t=>{
const f=world(t,{removeResolverPrepared:true}),d=await f.raise(),before=f.writer.read('demo').revision;
await assert.rejects(f.call('judge','decision.resolve',{id:d.id,choice:'yes'}),{code:'not-resolver'});
assert.equal(f.writer.read('demo').revision,before);f.writer.recover('demo');assert.equal(f.writer.read('demo').revision,before);
});
test('AU authority hire reverse runs normal fire/revoke and records committed delisting',async t=>{
const f=world(t),d=await f.raise({op:'hire',name:'helper'});await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});
assert.ok(f.writer.read('demo').document.launch.instances.includes('helper'));
const r=await f.reverse(d.id);assert.equal(r.committed,true);assert.ok(!f.writer.read('demo').document.launch.instances.includes('helper'));assert.deepEqual(f.order,['fire-revoke']);
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse'").n,1);
});
test('AU completion-event failure after durable authority inverse records committed recovery, never reverse.failed',async t=>{
const f=world(t),d=await f.raise();await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});
const run=f.store.run.bind(f.store);let injected=false;
f.store.run=(sql,...args)=>{
if(!injected && sql.includes('INSERT INTO events') && args.some(x=>typeof x==='string' && x.startsWith('{') && JSON.parse(x).action==='fire.done')){
injected=true;throw new Error('injected completion event failure');
}
return run(sql,...args);
};
const r=await f.reverse(d.id);assert.equal(injected,true);assert.equal(r.committed,true);assert.equal(r.completion,'failed');assert.equal(r.restart,true);assert.equal(r.relaunch,'pending');
assert.equal(f.writer.read('demo').document.definitions.scout.instructions,'Prior role');
assert.equal(f.writer.read('demo').document.definitions.scout.rev,5);
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse'").n,1);
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse.failed'").n,0);
});
test('AU inverse receipt-write failure returns committed with trailPending and never writes reverse.failed',async t=>{
const f=world(t),d=await f.raise();await f.call('judge','decision.resolve',{id:d.id,choice:'yes'});
const run=f.store.run.bind(f.store);let injected=false;
f.store.run=(sql,...args)=>{
if(sql.includes('INSERT INTO events') && args.some(x=>typeof x==='string' && x.startsWith('{') && JSON.parse(x).operation==='autonomy.reverse')){
injected=true;throw new Error('injected receipt write failure');
}
return run(sql,...args);
};
const r=await f.reverse(d.id);assert.equal(injected,true);assert.equal(r.committed,true);assert.equal(r.trailPending,true);assert.equal(r.relaunch,'requested');
assert.equal(f.writer.read('demo').document.definitions.scout.instructions,'Prior role');
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse.failed'").n,0);
});
test('AU an account-quota authority raise routes to the resolver only when the host says every harness spends the account (#1873)',async t=>{
const covered=world(t,{account:true,countsAccount:name=>name==='pi'}),d=await covered.raise();assert.equal(d.route_to,'judge');
for(const countsAccount of [undefined,()=>false,()=>null]){
const f=world(t,{account:true,countsAccount});
await assert.rejects(f.raise(),{code:'autonomy-precondition',reason:'no enforced budget covers new agents; authority away from the loop needs one'});
assert.equal(f.store.get("SELECT count(*) n FROM decisions").n,0);
}
});
// #1873 review finding 2: the runtime itself carries each harness module's countsAccount into the
// broker's authority budget check. Only harness modules decide; the business names none here but
// through plugins.harness.
for(const [harness,code] of [['claude-code',null],['pi','autonomy-precondition']])test(`AU runtime: an account-quota authority setting with only ${harness} enabled ${code?'refuses':'saves'} (#1873)`,async t=>{
const {startBroker}=await import('../src/runtime.mjs');
const {systemFor}=await import('../../organization/tests/helpers.mjs');
const root=mkdtempSync(join(tmpdir(),'au-runtime-')),rolesDir=rolesCopy(root);t.after(()=>rmSync(root,{recursive:true,force:true}));
const doc=businessDoc(root,'demo');doc.plugins={harness:[harness],'limit-check':['token-limit']};
for(const r of Object.values(doc.roles))if(r.vars?.harness)r.vars.harness=harness;
doc.launch.account={name:'shared',credential:'FIXTURE_TOKEN'};doc.launch.limits.tokens={keys:{shared:cap}};
doc.definitions={judge:{rev:1,role:{...inline,authority:{withinRole:['decision.resolve.authority'],crossRole:[]}},instructions:'Judge'}};
delete doc.definitions.judge.role.instructions;
doc.roles.judge={definition:'org:judge',vars:{harness,model:'claude-opus-5-5'}};
writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
const loaded=()=>{const b=loadBusiness('demo',{dir:root,rolesDir});return {...b,roles:Object.fromEntries(Object.entries(b.roles).map(([n,e])=>[n,{...b.definitions[e.definition],...e}]))};};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:loaded()},organization:{dir:root,rolesDir,system:systemFor(root)}});t.after(()=>bus.close());
const owner=bus.broker.bindConsole({business:'demo',human:'jason',subject:{provider:'test',iss:'test',sub:'owner'}});
const {revision}=await bus.broker.request(owner,{verb:'autonomy.get.console'});
const set=bus.broker.request(owner,{verb:'autonomy.set.console',args:{revision,autonomy:{authority:'near the loop',authorityResolver:'judge',notices:true}}});
if(code)await assert.rejects(set,{code});
else {await set;assert.equal(JSON.parse(readFileSync(join(root,'businesses/demo.json'),'utf8')).autonomy.authority,'near the loop');}
});