返回 CodeWhale
shell-hooks-host.test.mjs
根目录 / crates / tui / extension-host / test / shell-hooks-host.test.mjs
1 // Real selected Node host/HostRoot/Loader; fake Core register authority. No shell process runs.
2 import test from 'node:test'
3 import assert from 'node:assert/strict'
4 import {realpathSync,mkdtempSync,writeFileSync,mkdirSync,rmSync} from 'node:fs'
5 import {tmpdir} from 'node:os'
6 import {join} from 'node:path'
7 import {createHash} from 'node:crypto'
8 import {startHost,activate} from './harness.mjs'
9 const hash=value=>createHash('sha256').update(value).digest('hex')
10 function composition(t,dialect,{bad=false}={}) {
11 const root=realpathSync(mkdtempSync(join(tmpdir(),'cw-hook-host-')))
12 t.after(()=>rmSync(root,{recursive:true,force:true}));mkdirSync(join(root,'source'))
13 const config=JSON.stringify({hooks:{PreToolUse:[{matcher:'write',hooks:[{type:'command',command:'true',timeout:3}]}]}})
14 writeFileSync(join(root,'source','hooks.json'),config)
15 const layer=JSON.stringify([{insert:[{id:'dialect-hooks',name:`@deepseek-ai/dsh-hooks-${dialect}`,config:{configPath:'hooks.json'}}]}])
16 const spec={version:1,layers:[{path:'base.json',sha256:hash(layer),source:layer}],modules:[],files:{'hooks.json':hash(config)}}
17 writeFileSync(join(root,'composition.json'),JSON.stringify(spec))
18 const entry=join(root,'index.mjs')
19 writeFileSync(entry,`import {mountReviewedComposition} from '@codewhale/dsh-composition';import spec from './composition.json' with {type:'json'};export async function apply(ctx){await mountReviewedComposition(ctx,new URL('./source/',import.meta.url).href,spec)}`)
20 if(bad)writeFileSync(join(root,'source','hooks.json'),'{}')
21 return entry
22 }
23
24 test('reviewed Claude and Codex bridge rows mount in one real HostRoot and withdraw exact owner handles',async t=>{
25 const host=await startHost();t.after(()=>host.stop())
26 const a=await activate(host,'claude-native-hooks',composition(t,'claude-code'))
27 const b=await activate(host,'codex-native-hooks',composition(t,'codex'))
28 assert.equal(a.result.status,'ok',a.result.diagnostic+' '+JSON.stringify(host.logs)+' '+host.stderr);assert.equal(b.result.status,'ok',b.result.diagnostic)
29 const rows=host.registry.filter(r=>r.op==='register' && r.kind==='shell_hook')
30 assert.equal(rows.length,2);assert.deepEqual(rows.map(r=>r.owner.plugin_id),['claude-native-hooks','codex-native-hooks'])
31 for(const row of rows){const spec=JSON.parse(row.spec.description);assert.equal(spec.point,'PreToolUse');assert.equal(spec.matcher,'write');assert.equal(spec.hook.command,'true');assert.equal(spec.hook.event,'tool_call_before')}
32 assert.deepEqual(await host.call('ext/deactivate',{owner:a.ref}),{disposed:true,leaked:[]})
33 assert.ok(host.registry.some(r=>r.op==='unregister' && r.handle===rows[0].handle))
34 assert.ok(!host.registry.some(r=>r.op==='unregister' && r.handle===rows[1].handle))
35 assert.deepEqual(await host.call('ext/deactivate',{owner:b.ref}),{disposed:true,leaked:[]})
36 assert.ok(host.registry.some(r=>r.op==='unregister' && r.handle===rows[1].handle))
37 })
38
39 test('changed reviewed config fails activation without empty-tree success or harming another hook owner',async t=>{
40 const host=await startHost();t.after(()=>host.stop())
41 const good=await activate(host,'good-hooks',composition(t,'claude-code'))
42 assert.equal(good.result.status,'ok',good.result.diagnostic+' '+JSON.stringify(host.logs)+' '+host.stderr)
43 const goodRow=host.registry.find(r=>r.op==='register' && r.kind==='shell_hook')
44 const changed=await activate(host,'changed-hooks',composition(t,'codex',{bad:true}))
45 assert.equal(changed.result.status,'failed');assert.match(changed.result.diagnostic,/changed|closure|refused/i)
46 assert.equal(host.registry.filter(r=>r.op==='register' && r.owner.plugin_id==='changed-hooks').length,0)
47 assert.ok(!host.registry.some(r=>r.op==='unregister' && r.handle===goodRow.handle))
48 assert.deepEqual(await host.call('ext/deactivate',{owner:good.ref}),{disposed:true,leaked:[]})
49 assert.ok(host.registry.some(r=>r.op==='unregister' && r.handle===goodRow.handle))
50 })
51
51 lines Plain Text