Files
stack/packages/organization/tests/autonomy-inverse.test.mjs

66 lines
5.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {rmSync} from 'node:fs';
import {join} from 'node:path';
import {tmp,rolesCopy,businessDoc,writeJson} from './helpers.mjs';
import {organizationWriter} from '../src/organization.mjs';
import {hireDecisions} from '../src/hire-decisions.mjs';
import {instanceSettings} from '../src/instance-settings.mjs';
const role={tools:['read'],network:'none',authority:{withinRole:[],crossRole:[]},credentials:[],instructions:'Prior role'};
function setup(t, scanDefinition, barrier) {
const root=tmp();t.after(()=>rmSync(root,{recursive:true,force:true}));const rolesDir=rolesCopy(root),doc=businessDoc(root);
doc.plugins={harness:['pi']};
doc.definitions={scout:{rev:3,role:{tools:role.tools,network:role.network,authority:role.authority,credentials:[]},instructions:role.instructions}};
doc.roles.scout={definition:'org:scout',vars:{harness:'pi',model:'claude-opus-5-5'}};doc.launch.instances.push('scout');
writeJson(join(root,'businesses/acme.json'),doc);
const writer=organizationWriter({dir:root,rolesDir,barrier}),service=hireDecisions({writer,rolesDir,configDir:root,dataRoot:join(root,'data'),assertClean:()=>{},scanDefinition});
let stopped=0,revoked=0;const instances=instanceSettings({dir:root,rolesDir,writer,revoke:()=>{revoked++;}});
const prepare=async request=>{const p=await service.prepare('acme',Object.fromEntries(Object.entries({op:'change',level:'org',name:'scout',role:{inline:{...role,instructions:'Changed role'}},route:{harness:'pi',model:'claude-opus-5-5'},reason:'Test',...request}).filter(([,v])=>v!==undefined)),{id:randomUUID(),expiresAt:'2099-01-01T00:00:00Z'});return p;};
const caller=p=>({agent:false,rights:['org.hire'],owner:true,instanceOwner:true,actor:'jason',digest:p.digest,subject:{provider:'test',iss:'test',sub:'owner'}});
const reverse=p=>service.reverse('acme',p.id,{actor:'jason',beforeWrite(){},stop:async()=>{stopped++;return {running:true};},fire:proposal=>instances.fire('acme',{name:proposal.name,revision:proposal.revision},{actor:'jason',context:{decision:proposal.id}})});
return {writer,service,prepare,caller,reverse,stopped:()=>stopped,revoked:()=>revoked};
}
test('AU role-change inverse restores prior definition contents and requests relaunch of a running agent',async t=>{
const f=setup(t),p=await f.prepare({});await f.service.apply('acme',p.id,f.caller(p));
const r=await f.reverse(p),doc=f.writer.read('acme').document;
assert.equal(doc.definitions.scout.instructions,'Prior role');assert.equal(doc.definitions.scout.rev,5);assert.equal(r.relaunch.name,'scout');assert.equal(f.stopped(),1);
});
test('AU fire inverse rehires prior definition without restoring revoked bindings',async t=>{
const f=setup(t),p=await f.prepare({op:'fire',role:undefined,route:undefined});await f.service.apply('acme',p.id,f.caller(p));
assert.ok(!f.writer.read('acme').document.launch.instances.includes('scout'));
await f.reverse(p);const doc=f.writer.read('acme').document;
assert.ok(doc.launch.instances.includes('scout'));assert.equal(doc.definitions.scout.instructions,'Prior role');assert.equal(doc.roles.scout.credentials,undefined);
});
test('AU hire inverse uses the normal fire/revoke executor',async t=>{
const f=setup(t),p=await f.prepare({op:'hire',name:'new-scout'});await f.service.apply('acme',p.id,f.caller(p));
await f.reverse(p);assert.ok(!f.writer.read('acme').document.launch.instances.includes('new-scout'));assert.equal(f.revoked(),1);
});
test('AU inverse never overwrites a subsequent role change or relaunched fired agent',async t=>{
const f=setup(t),p=await f.prepare({});await f.service.apply('acme',p.id,f.caller(p));
const later=await f.prepare({role:{inline:{...role,instructions:'Later'}}});await f.service.apply('acme',later.id,f.caller(later));
await assert.rejects(f.reverse(p),{code:'decision-stale'});assert.equal(f.stopped(),0);
const fire=await f.prepare({op:'fire',role:undefined,route:undefined});await f.service.apply('acme',fire.id,f.caller(fire));
const old=f.writer.read('acme');f.writer.write('acme',{...old.document,launch:{...old.document.launch,instances:[...old.document.launch.instances,'scout']}},{revision:old.revision,actor:'jason'});
await assert.rejects(f.reverse(fire),{code:'decision-stale'});
});
test('AU inverse scans prior definition against current held credentials before stopping or writing',async t=>{
let refuse=false;
const f=setup(t,()=>{if(refuse)throw Object.assign(new Error('definition-holds-credential'),{code:'definition-holds-credential'});});
const p=await f.prepare({});await f.service.apply('acme',p.id,f.caller(p));
const before=f.writer.read('acme').revision;refuse=true;
await assert.rejects(f.reverse(p),{code:'definition-holds-credential'});
assert.equal(f.stopped(),0);assert.equal(f.writer.read('acme').revision,before);
});
test('AU inverse crossing its window during candidate IO never publishes or recovers forward',async t=>{
let reversing=false,expired=false;
const f=setup(t,undefined,point=>{if(reversing && point==='prepared')expired=true;});
const p=await f.prepare({});await f.service.apply('acme',p.id,f.caller(p));
const prior=f.writer.read('acme');reversing=true;
await assert.rejects(f.service.reverse('acme',p.id,{actor:'jason',beforeWrite(){if(expired)throw Object.assign(new Error('reverse-window-closed'),{code:'reverse-window-closed'});},stop:async()=>({running:true})}),{code:'reverse-window-closed'});
assert.equal(f.writer.read('acme').revision,prior.revision);
f.writer.recover('acme');assert.equal(f.writer.read('acme').revision,prior.revision);
});