| 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 |