| 1 | // Acceptance candidates only. Real host/fake core, not Rust admission or grants proof. |
| 2 | import { test } from 'node:test' |
| 3 | import assert from 'node:assert/strict' |
| 4 | import { createHash } from 'node:crypto' |
| 5 | import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' |
| 6 | import { tmpdir } from 'node:os' |
| 7 | import { join } from 'node:path' |
| 8 | import { activate, startHost } from './harness.mjs' |
| 9 | |
| 10 | function entries(t) { |
| 11 | const root=mkdtempSync(join(tmpdir(),'cw-entry-scopes-')) |
| 12 | t.after(()=>rmSync(root,{recursive:true,force:true})) |
| 13 | const make=(name,body)=>{ |
| 14 | const path=join(root,`${name}.mjs`) |
| 15 | const source=`export const inject=['tools','prompt'];export function apply(ctx) { ${body} }` |
| 16 | writeFileSync(path,source) |
| 17 | return {path,sha256:createHash('sha256').update(source).digest('hex')} |
| 18 | } |
| 19 | return {root,make} |
| 20 | } |
| 21 | |
| 22 | test('one owner can mount two scoped entries with equal names and dispose only one',async t=>{ |
| 23 | const host=await startHost();t.after(()=>host.stop()) |
| 24 | const {root,make}=entries(t) |
| 25 | const body=value=>`ctx.tools.register({name:'scoped_echo',description:'Entry echo',parameters:{type:'object',properties:{}},execute(){return ${JSON.stringify(value)}}});ctx.prompt.registerSection({id:'note',text:${JSON.stringify(value)}})` |
| 26 | const a=make('a',body('A')),b=make('b',body('B')) |
| 27 | const first=await activate(host,'scoped-entries',a.path,{scope:a,data_dir:root}) |
| 28 | assert.equal(first.result.status,'ok',first.result.diagnostic) |
| 29 | const second=await host.call('ext/activate',{owner:first.ref,plugin_name:'scoped-entries',entry:b,scope:b,config:{},data_dir:root}) |
| 30 | assert.equal(second.status,'ok',second.diagnostic) |
| 31 | const tools=host.registry.filter(row=>row.op==='register' && row.kind==='tool') |
| 32 | assert.equal(tools.length,2) |
| 33 | assert.deepEqual(tools.map(row=>row.scope),[a,b]) |
| 34 | assert.notEqual(tools[0].handle,tools[1].handle) |
| 35 | const call=handle=>host.call('tool/call',{handle,input:{},call_id:`scope-${handle}`,deadline_ms:5000}) |
| 36 | assert.match(JSON.stringify(await call(tools[0].handle)),/A/) |
| 37 | assert.match(JSON.stringify(await call(tools[1].handle)),/B/) |
| 38 | assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref,entry:a}),{disposed:true,leaked:[]}) |
| 39 | await assert.rejects(call(tools[0].handle),/not live/) |
| 40 | assert.match(JSON.stringify(await call(tools[1].handle)),/B/) |
| 41 | assert.ok(host.registry.some(row=>row.op==='unregister' && row.handle===tools[0].handle)) |
| 42 | assert.ok(!host.registry.some(row=>row.op==='unregister' && row.handle===tools[1].handle)) |
| 43 | assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref}),{disposed:true,leaked:[]}) |
| 44 | await assert.rejects(call(tools[1].handle),/not live/) |
| 45 | }) |
| 46 | |
| 47 | test('failed scoped activation keeps the same owner sibling live and mismatched scopes refuse',async t=>{ |
| 48 | const host=await startHost();t.after(()=>host.stop()) |
| 49 | const {root,make}=entries(t) |
| 50 | const good=make('good',"ctx.tools.register({name:'kept_echo',description:'kept',parameters:{type:'object',properties:{}},execute(){return 'kept'}})") |
| 51 | const bad=make('bad',"ctx.prompt.registerSection({id:'partial',text:'must retire'});throw new Error('intentional scoped failure')") |
| 52 | const first=await activate(host,'scoped-failure',good.path,{scope:good,data_dir:root}) |
| 53 | assert.equal(first.result.status,'ok',first.result.diagnostic) |
| 54 | const refused=await host.call('ext/activate',{owner:first.ref,plugin_name:'scoped-failure',entry:bad,scope:good,config:{},data_dir:root}) |
| 55 | assert.equal(refused.status,'failed');assert.match(refused.diagnostic,/scope does not match/) |
| 56 | const failed=await host.call('ext/activate',{owner:first.ref,plugin_name:'scoped-failure',entry:bad,scope:bad,config:{},data_dir:root}) |
| 57 | assert.equal(failed.status,'failed');assert.match(failed.diagnostic,/intentional scoped failure/) |
| 58 | assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref,entry:bad}),{disposed:true,leaked:[]}) |
| 59 | const partial=host.registry.find(row=>row.op==='register' && row.kind==='prompt_section') |
| 60 | assert.ok(host.registry.some(row=>row.op==='unregister' && row.handle===partial.handle)) |
| 61 | const tool=host.registry.find(row=>row.op==='register' && row.kind==='tool') |
| 62 | assert.match(JSON.stringify(await host.call('tool/call',{handle:tool.handle,input:{},call_id:'kept',deadline_ms:5000})),/kept/) |
| 63 | assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref}),{disposed:true,leaked:[]}) |
| 64 | }) |
| 65 |