返回 CodeWhale
composition-membership-host.test.mjs
根目录 / crates / tui / extension-host / test / composition-membership-host.test.mjs
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
65 lines Plain Text