返回 CodeWhale
composition-host.test.mjs
根目录 / crates / tui / extension-host / test / composition-host.test.mjs
1 // Real Node/Bun host and reviewed source closure; fake core transport.
2 // Rust receipt/admission/membership/install qualification is a separate gate.
3 import { test } from 'node:test'
4 import assert from 'node:assert/strict'
5 import { spawnSync } from 'node:child_process'
6 import { createHash } from 'node:crypto'
7 import { existsSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
8 import { tmpdir } from 'node:os'
9 import { dirname, join } from 'node:path'
10 import { fileURLToPath } from 'node:url'
11 import { activate, startHost, FIXTURES, IS_BUN } from './harness.mjs'
12
13 const here = dirname(fileURLToPath(import.meta.url))
14 const hash = (value) => createHash('sha256').update(value).digest('hex')
15
16 function temporary(t) {
17 const root = mkdtempSync(join(tmpdir(), 'cw-composition-'))
18 t.after(() => rmSync(root, { recursive: true, force: true }))
19 return root
20 }
21
22 test('canonical Native composition adopts existing contributions and withdraws their exact handles', async (t) => {
23 const host = await startHost()
24 t.after(() => host.stop())
25 const data_dir = temporary(t)
26 const entry = join(FIXTURES, 'native-dsh-composition', 'native', 'index.mjs')
27 const { ref, result } = await activate(host, 'native-dsh-composition', entry, { data_dir })
28 assert.equal(result.status, 'ok', result.diagnostic)
29 const admitted = host.registry.filter(item => item.op === 'register')
30 assert.deepEqual(new Set(admitted.map(item => item.kind)), new Set(['tool', 'command', 'prompt_section', 'skill_root']))
31 assert.equal(admitted.find(item => item.kind === 'prompt_section').spec.description, 'hello from upstream')
32 assert.equal(admitted.find(item => item.kind === 'skill_root').spec.name, 'source/skills')
33 const tool = admitted.find(item => item.kind === 'tool')
34 const answer = await host.call('tool/call', { handle: tool.handle, input: {}, call_id: 'composition-call', deadline_ms: 5000, workspace: data_dir })
35 assert.deepEqual(answer.structured, { text: 'hello from upstream', count: null })
36 assert.deepEqual(await host.call('ext/deactivate', { owner: ref }), { disposed: true, leaked: [] })
37 for (const item of admitted) assert.ok(host.registry.some(row => row.op === 'unregister' && row.handle === item.handle))
38 })
39
40 function counterComposition(t, id) {
41 const root = temporary(t)
42 mkdirSync(join(root, 'source'))
43 const source = `export const inject = ['prompt']; export function apply(ctx, config) { ctx.prompt.registerSection({id:${JSON.stringify(id)},text:String(config.count)}) }`
44 writeFileSync(join(root, 'source', 'row.mjs'), source)
45 const layer = `- insert:\n - id: count\n name: ./row.mjs\n config:\n count: !!js '(globalThis.__CW_COMPOSITION_COUNT = (globalThis.__CW_COMPOSITION_COUNT ?? 0) + 1)'\n`
46 const spec = { version: 1, layers: [{ path: 'base.yml', sha256: hash(layer), source: layer }], modules: [{ name: './row.mjs', path: 'row.mjs', sha256: hash(source) }], files: { 'row.mjs': hash(source) } }
47 writeFileSync(join(root, 'composition.json'), JSON.stringify(spec))
48 const entry = join(root, 'index.mjs')
49 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)}`)
50 return entry
51 }
52
53 test('two compositions share exactly one Loader and one teardown keeps the other owner live', async (t) => {
54 const host = await startHost()
55 t.after(() => host.stop())
56 const a = await activate(host, 'composition-a', counterComposition(t, 'note-a'))
57 const b = await activate(host, 'composition-b', counterComposition(t, 'note-b'))
58 assert.equal(a.result.status, 'ok', a.result.diagnostic)
59 assert.equal(b.result.status, 'ok', b.result.diagnostic)
60 const notes = host.registry.filter(item => item.op === 'register' && item.kind === 'prompt_section')
61 assert.deepEqual(notes.map(item => item.spec.description), ['1', '2'], 'duplicate global Loader observers evaluated a configuration twice')
62 assert.deepEqual(await host.call('ext/deactivate', { owner: a.ref }), { disposed: true, leaked: [] })
63 assert.ok(!host.registry.some(item => item.op === 'unregister' && item.handle === notes[1].handle))
64 const c = await activate(host, 'composition-c', counterComposition(t, 'note-c'))
65 assert.equal(c.result.status, 'ok', c.result.diagnostic)
66 assert.equal(host.registry.filter(item => item.op === 'register' && item.kind === 'prompt_section').at(-1).spec.description, '3')
67 })
68
69 test('trusted offline reviewer preserves expressions and never imports row modules', (t) => {
70 const root = temporary(t)
71 const sentinel = join(root, 'expression-executed')
72 const layer = JSON.stringify([{ insert: [{ id: 'unexecuted', name: './row.mjs', config: { __jsExpr: `process.getBuiltinModule('fs').writeFileSync(${JSON.stringify(sentinel)},'unsafe')` } }] }])
73 const spec = { version: 1, layers: [{ path: 'base.json', sha256: hash(layer), source: layer }], modules: [{ name: './row.mjs', path: 'row.mjs', sha256: '0'.repeat(64) }], files: { 'row.mjs': '0'.repeat(64) } }
74 const result = spawnSync(process.execPath, [join(here, '..', 'dist', 'dsh-composition-review.mjs')], { input: JSON.stringify(spec), encoding: 'utf8', timeout: 5000, maxBuffer: 8 * 1024 * 1024 })
75 assert.equal(result.status, 0, result.stderr)
76 const review = JSON.parse(result.stdout)
77 assert.deepEqual(review.entries[0].config, JSON.parse(layer)[0].insert[0].config)
78 assert.equal(existsSync(sentinel), false)
79 assert.equal(existsSync(join(root, 'row.mjs')), false, 'review did not need any row module to exist')
80 })
81
82 function moduleClosureComposition(t, rowSource, files, extraRows = {}) {
83 const root=temporary(t)
84 mkdirSync(join(root,'source'))
85 const rows={'row.mjs':rowSource,...extraRows}
86 for (const [name,source] of Object.entries({...rows,...files})) writeFileSync(join(root,'source',name),source)
87 const layer=JSON.stringify([{insert:Object.keys(rows).map((name,index)=>({id:`row-${index}`,name:`./${name}`,config:{}}))}])
88 const closure=Object.fromEntries(Object.entries({...rows,...files}).map(([name,source])=>[name,hash(source)]))
89 const spec={version:1,layers:[{path:'base.json',sha256:hash(layer),source:layer}],modules:Object.entries(rows).map(([name,source])=>({name:`./${name}`,path:name,sha256:hash(source)})),files:closure}
90 writeFileSync(join(root,'composition.json'),JSON.stringify(spec))
91 const entry=join(root,'index.mjs')
92 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)}`)
93 return {root,entry}
94 }
95
96 test('a transitive reviewed relative helper executes but an undeclared helper never does',async t=>{
97 const host=await startHost();t.after(()=>host.stop())
98 const good=moduleClosureComposition(t,"import {text} from './helper.mjs';export const inject=['prompt'];export function apply(ctx){ctx.prompt.registerSection({id:'nested',text})}",{'helper.mjs':"export const text='reviewed helper'"})
99 const mounted=await activate(host,'closure-good',good.entry)
100 assert.equal(mounted.result.status,'ok',mounted.result.diagnostic)
101 assert.equal(host.registry.find(row=>row.op==='register' && row.kind==='prompt_section').spec.description,'reviewed helper')
102 const bad=moduleClosureComposition(t,"import './rogue.mjs';export function apply(){}",{})
103 const sentinel=join(bad.root,'rogue-executed')
104 writeFileSync(join(bad.root,'source','rogue.mjs'),`import {writeFileSync} from 'node:fs';writeFileSync(${JSON.stringify(sentinel)},'bad')`)
105 const refused=await activate(host,'closure-undeclared',bad.entry)
106 assert.equal(refused.result.status,'failed')
107 assert.equal(existsSync(sentinel),false)
108 assert.deepEqual(await host.call('ext/deactivate',{owner:mounted.ref}),{disposed:true,leaked:[]})
109 })
110
111 test('changed transitive module bytes and ambient bare dependencies refuse before execution',async t=>{
112 const host=await startHost();t.after(()=>host.stop())
113 const changed=moduleClosureComposition(t,"import './helper.mjs';export function apply(){}",{'helper.mjs':"export const original=true"})
114 const sentinel=join(changed.root,'changed-executed')
115 writeFileSync(join(changed.root,'source','helper.mjs'),`import {writeFileSync} from 'node:fs';writeFileSync(${JSON.stringify(sentinel)},'bad')`)
116 const refused=await activate(host,'closure-changed',changed.entry)
117 assert.equal(refused.result.status,'failed')
118 assert.equal(existsSync(sentinel),false)
119 const ambient=moduleClosureComposition(t,"import 'js-yaml';export function apply(){}",{})
120 const unavailable=await activate(host,'closure-ambient',ambient.entry)
121 assert.equal(unavailable.result.status,'failed')
122 })
123
124
125 test('reviewed composition refuses a row creation failure instead of acknowledging an empty tree', async t => {
126 const host=await startHost();t.after(()=>host.stop())
127 const entry=counterComposition(t,'creation-probe')
128 const source=String.raw`export const inject=['prompt'];export function apply(ctx){throw new Error('intentional row startup refusal')}`
129 // Full rows stay reviewed; the failure occurs inside activation, not the offline reviewer.
130 const root=dirname(entry)
131 writeFileSync(join(root,'source','row.mjs'),source)
132 const layer=JSON.stringify([{insert:[{id:'must-start',name:'./row.mjs',config:{}}]}])
133 writeFileSync(join(root,'composition.json'),JSON.stringify({version:1,layers:[{path:'base.json',sha256:hash(layer),source:layer}],modules:[{name:'./row.mjs',path:'row.mjs',sha256:hash(source)}],files:{'row.mjs':hash(source)}}))
134 const result=await activate(host,'startup-refusal',entry)
135 assert.equal(result.result.status,'failed')
136 assert.equal(host.registry.filter(row=>row.op==='register').length,0)
137 })
138
139
140 test('reviewed computed and URL imports retain query identity and exact JSON data', async t => {
141 const host=await startHost();t.after(()=>host.stop())
142 const row=`export const inject=['prompt'];export async function apply(ctx){
143 const name='./helper.mjs';const first=await import(name);
144 const url=new URL('./helper.mjs?variant=two',import.meta.url);
145 const queried=await import(url);const again=await import(String(url));
146 const data=await import('./data.json',{with:{type:'json'}});
147 ctx.prompt.registerSection({id:'dynamic',text:JSON.stringify({text:first.text,queryDistinct:first!==queried,queryCached:queried===again,protoData:Object.hasOwn(data.default,'__proto__'),value:data.default.__proto__.answer})});
148 }`
149 const composition=moduleClosureComposition(t,row,{'helper.mjs':"export const text='computed reviewed helper'",'data.json':'{"__proto__":{"answer":42}}'})
150 const mounted=await activate(host,'closure-computed',composition.entry)
151 if(IS_BUN){
152 assert.equal(mounted.result.status,'failed')
153 assert.match(mounted.result.diagnostic,/Bun does not preserve reviewed module query or fragment identity/)
154 assert.match(mounted.result.diagnostic,/runtime = "node"/)
155 assert.equal(host.registry.filter(row=>row.op==='register').length,0)
156 return
157 }
158 assert.equal(mounted.result.status,'ok',mounted.result.diagnostic)
159 assert.deepEqual(JSON.parse(host.registry.find(row=>row.op==='register' && row.kind==='prompt_section').spec.description),{text:'computed reviewed helper',queryDistinct:true,queryCached:true,protoData:true,value:42})
160 assert.deepEqual(await host.call('ext/deactivate',{owner:mounted.ref}),{disposed:true,leaked:[]})
161 })
162
163 test('unreviewed computed imports refuse before module side effects', async t => {
164 const host=await startHost();t.after(()=>host.stop())
165 const composition=moduleClosureComposition(t,"export async function apply(){const name='./rogue.mjs';await import(name)}",{})
166 const sentinel=join(composition.root,'computed-rogue-executed')
167 writeFileSync(join(composition.root,'source','rogue.mjs'),`import {writeFileSync} from 'node:fs';writeFileSync(${JSON.stringify(sentinel)},'bad')`)
168 const mounted=await activate(host,'closure-computed-rogue',composition.entry)
169 assert.equal(mounted.result.status,'failed')
170 assert.equal(existsSync(sentinel),false)
171 assert.equal(host.registry.filter(row=>row.op==='register').length,0)
172 })
173
174 test('separate reviewed rows retain the same transitive module instance', async t => {
175 const host=await startHost();t.after(()=>host.stop())
176 const row=id=>`import {next} from './helper.mjs';export const inject=['prompt'];export function apply(ctx){ctx.prompt.registerSection({id:${JSON.stringify(id)},text:String(next())})}`
177 const composition=moduleClosureComposition(t,row('first'),{'helper.mjs':'let count=0;export function next(){return ++count}'},{'second.mjs':row('second')})
178 const mounted=await activate(host,'closure-shared-instance',composition.entry)
179 assert.equal(mounted.result.status,'ok',mounted.result.diagnostic)
180 const registrations=host.registry.filter(row=>row.op==='register' && row.kind==='prompt_section')
181 assert.deepEqual(registrations.map(row=>row.spec.description),['1','2'])
182 assert.deepEqual(await host.call('ext/deactivate',{owner:mounted.ref}),{disposed:true,leaked:[]})
183 for(const row of registrations)assert.ok(host.registry.some(item=>item.op==='unregister' && item.handle===row.handle))
184 })
185
186
187 test('reviewed computed URL imports and JSON preserve data on both runtimes', async t => {
188 const host=await startHost();t.after(()=>host.stop())
189 const row=`export const inject=['prompt'];export async function apply(ctx){
190 const target=new URL('./helper.mjs',import.meta.url);const helper=await import(target);
191 const again=await import(String(target));const data=await import('./data.json',{with:{type:'json'}});
192 ctx.prompt.registerSection({id:'computed-url',text:JSON.stringify({text:helper.text,cached:helper===again,protoData:Object.hasOwn(data.default,'__proto__'),value:data.default.__proto__.answer})});
193 }`
194 const composition=moduleClosureComposition(t,row,{'helper.mjs':"export const text='reviewed URL helper'",'data.json':'{"__proto__":{"answer":42}}'})
195 const mounted=await activate(host,'closure-computed-url',composition.entry)
196 assert.equal(mounted.result.status,'ok',mounted.result.diagnostic)
197 assert.deepEqual(JSON.parse(host.registry.find(row=>row.op==='register' && row.kind==='prompt_section').spec.description),{text:'reviewed URL helper',cached:true,protoData:true,value:42})
198 assert.deepEqual(await host.call('ext/deactivate',{owner:mounted.ref}),{disposed:true,leaked:[]})
199 })
200
200 lines Plain Text