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