返回 CodeWhale
agent-presets.test.mjs
根目录 / crates / tui / extension-host / test / agent-presets.test.mjs
1 // Source-focused Node host/fake Core acceptance. Rust admission/selection is separate.
2 import {test} from 'node:test'
3 import assert from 'node:assert/strict'
4 import {createHash} from 'node:crypto'
5 import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,rmSync} from 'node:fs'
6 import {join,dirname} from 'node:path'
7 import {tmpdir} from 'node:os'
8 import {fileURLToPath} from 'node:url'
9 import {reviewAgentPresets,containedPackageModule} from '../dist/agent-presets.mjs'
10 import {spawnSync} from 'node:child_process'
11 import {activate,startHost} from './harness.mjs'
12 const hash=s=>createHash('sha256').update(s).digest('hex')
13 function request(config,contents,extraDirs=[]) {
14 const layer=JSON.stringify([{insert:[{id:'raw-roster',name:'@deepseek-ai/dsh-agent-presets',config}]}])
15 const directories=new Set(extraDirs)
16 for(const path of Object.keys(contents)) { let dir=dirname(path);while(dir!=='.'){directories.add(dir);dir=dirname(dir)} }
17 const files=Object.fromEntries(Object.entries(contents).map(([path,text])=>[path,hash(text)]))
18 return {kind:'agent-presets',composition:{version:1,layers:[{path:'bundle.json',sha256:hash(layer),source:layer}],modules:[],files},directories:[...directories],documents:Object.entries(contents).filter(([path])=>/\/(?:package\.json|preset\.yml|agent\.cordis\.yml)$/.test(path)).map(([path,source])=>({path,source,sha256:hash(source)}))}
19 }
20 const cfg={includeShippedRoot:false,includeUserRoot:false,roots:[{path:'presets',trust:'user'}],default:'a'}
21 const empty='[]\n'
22
23 test('raw discovery preserves first-root-wins, metadata order, broken rows and ignored metadata trust',async()=>{
24 const input=request({...cfg,roots:[{path:'system',trust:'system'},{path:'user',trust:'user'}],default:'a'},{
25 'system/a/agent.cordis.yml':empty,'system/a/preset.yml':'name: Alpha\norder: 2\nid: spoof\ntrust: admin\n',
26 'system/b/agent.cordis.yml':empty,'system/b/preset.yml':'name: Beta\norder: 1\n',
27 'user/a/agent.cordis.yml':empty,'user/a/preset.yml':'name: Shadowed\n',
28 'user/c/agent.cordis.yml':empty,'user/c/preset.yml':'[malformed\n',
29 },['user/missing','user/INVALID'])
30 const review=await reviewAgentPresets(input)
31 assert.deepEqual(review.catalog.presets.map(p=>p.id),['b','a','c','missing'])
32 assert.equal(review.catalog.presets[1].name,'Alpha');assert.equal(review.catalog.presets[1].trust,'system')
33 assert.equal(review.catalog.presets[2].name,undefined);assert.match(review.catalog.presets[3].broken,/missing/)
34 assert.deepEqual(review.presets.map(p=>p.metadata.id),['b','a','c'])
35 })
36
37 test('confined bare lookup honors exact ESM export and refuses ambient/escape/require-only exports',async()=>{
38 const manifests=new Map([['node_modules/@test/profile/package.json',JSON.stringify({name:'@test/profile',type:'module',exports:{'.':{require:'./bad.cjs',import:'./main.mjs'},'./review':'./review.mjs'}})]])
39 const files={'node_modules/@test/profile/main.mjs':hash('main'),'node_modules/@test/profile/review.mjs':hash('review')}
40 assert.equal(containedPackageModule('@test/profile',manifests,files).path,'node_modules/@test/profile/main.mjs')
41 assert.equal(containedPackageModule('@test/profile/review',manifests,files).path,'node_modules/@test/profile/review.mjs')
42 for(const name of ['js-yaml','@test/profile/../../outside','file:///outside']) assert.equal(containedPackageModule(name,manifests,files),undefined)
43 const blocked=new Map([['node_modules/@test/profile/package.json',JSON.stringify({name:'@test/profile',type:'module',exports:{node:null,import:'./main.mjs'}})]])
44 assert.equal(containedPackageModule('@test/profile',blocked,files),undefined,'a blocked matched condition never falls through')
45 const input=request(cfg,{'presets/a/agent.cordis.yml':'- name: "@test/profile"\n','node_modules/@test/profile/package.json':manifests.get('node_modules/@test/profile/package.json'),'node_modules/@test/profile/main.mjs':'export function apply(){}'})
46 const review=await reviewAgentPresets(input)
47 assert.equal(review.presets[0].composition.modules[0].path,'node_modules/@test/profile/main.mjs')
48 })
49
50 test('raw roots and bytes stay admitted; disabled groups and conditional missing rows are not guessed',async()=>{
51 const input=request(cfg,{'presets/a/agent.cordis.yml':"- name: cordis:group\n group: true\n disabled: true\n config:\n - name: absent\n- name: absent-conditional\n disabled: !!js 'true'\n"})
52 const review=await reviewAgentPresets(input)
53 assert.equal(review.presets.length,1);assert.equal(review.presets[0].composition.modules.length,0)
54 const changed=structuredClone(input);changed.documents[0].source+=' # tampered'
55 await assert.rejects(reviewAgentPresets(changed),/document changed/)
56 for(const config of [{...cfg,roots:[{path:'../outside',trust:'user'}]},{...cfg,includeUserRoot:true},{...cfg,includeShippedRoot:true}]) {
57 await assert.rejects(reviewAgentPresets(request(config,{'presets/a/agent.cordis.yml':empty})),/admitted|inside/)
58 }
59 })
60
61 test('cyclic/oversized preset rows remain broken and cannot recurse discovery or replace Core prompt',async()=>{
62 const input=request(cfg,{'presets/a/agent.cordis.yml':empty,
63 'presets/cycle/agent.cordis.yml':'&rows\n- name: cordis:group\n group: true\n config: *rows\n',
64 'presets/minimal/agent.cordis.yml':'- name: "@deepseek-ai/dsh-persona"\n config: {complete: true}\n',
65 'node_modules/@deepseek-ai/dsh-persona/package.json':JSON.stringify({name:'@deepseek-ai/dsh-persona',type:'module',exports:'./index.mjs'}),
66 'node_modules/@deepseek-ai/dsh-persona/index.mjs':'export function apply(){}',
67 })
68 const review=await reviewAgentPresets(input)
69 assert.deepEqual(review.presets.map(p=>p.metadata.id),['a'])
70 assert.match(review.catalog.presets.find(p=>p.id==='cycle').broken,/cannot be read/)
71 assert.match(review.catalog.presets.find(p=>p.id==='minimal').broken,/Core prompt/)
72 const additive=request(cfg,{'presets/a/agent.cordis.yml':empty,'presets/persona/agent.cordis.yml':'- name: "@deepseek-ai/dsh-persona"\n config: {prefix: additive}\n','node_modules/@deepseek-ai/dsh-persona/package.json':JSON.stringify({name:'@deepseek-ai/dsh-persona',type:'module',exports:'./index.mjs'}),'node_modules/@deepseek-ai/dsh-persona/index.mjs':'export function apply(){}'})
73 const bounded=await reviewAgentPresets(additive)
74 assert.equal(bounded.catalog.presets.find(p=>p.id==='persona').broken,undefined)
75 assert.deepEqual(bounded.presets.map(p=>p.metadata.id),['a','persona'])
76 await assert.rejects(reviewAgentPresets({...input,composition:{...input.composition,layers:input.composition.layers.map(l=>{const source=l.source.replace('"default":"a"','"default":"minimal"');return {...l,source,sha256:hash(source)}})}}),/default preset is missing or broken/)
77 })
78
79 function authoredRow(value) {
80 return `export const inject=['tools','commands','prompt','skills','agentPresets'];
81 export function apply(ctx){
82 const value=${JSON.stringify(value)}+':'+ctx.agentPresets.composedPreset();
83 ctx.tools.register({name:'preset_echo',description:'exact selected preset',parameters:{type:'object',properties:{}},execute:()=>value});
84 ctx.commands.register({name:'preset-echo',description:'exact selected preset',handler:()=>value});
85 ctx.prompt.registerSection({id:'selected',text:value});
86 ctx.skills.registerRoot({path:'source/skills/${value}'});
87 ctx.on('tools/pre-execute',()=>({kind:'annotate',text:value}));
88 }`
89 }
90 function installedEntries(t,review,contents) {
91 const root=mkdtempSync(join(tmpdir(),'cw-raw-presets-'));t.after(()=>rmSync(root,{recursive:true,force:true}))
92 for(const [path,source] of Object.entries(contents)){const target=join(root,'source',path);mkdirSync(dirname(target),{recursive:true});writeFileSync(target,source)}
93 mkdirSync(join(root,'native/presets'),{recursive:true})
94 return review.presets.map(selected=>{
95 const id=selected.metadata.id
96 const entry=join(root,'native/presets',`${id}.mjs`)
97 writeFileSync(join(root,'native/presets',`${id}.json`),JSON.stringify({catalog:review.catalog,selected,composition:review.top}))
98 const source=`import {mountReviewedPreset} from '@codewhale/dsh-composition';import data from './${id}.json' with {type:'json'};export async function apply(ctx){await mountReviewedPreset(ctx,new URL('../../source/',import.meta.url).href,data.composition,data.catalog,data.selected)}`
99 writeFileSync(entry,source);return {path:entry,sha256:hash(source)}
100 })
101 }
102
103 test('real raw roster mounts all five registries under one exact selected entry and tears down only that sibling',async t=>{
104 const host=await startHost();t.after(()=>host.stop())
105 const contents={
106 'presets/a/agent.cordis.yml':'- name: ./row.mjs\n','presets/a/row.mjs':authoredRow('A'),
107 'presets/b/agent.cordis.yml':'- name: "@demo/preset-row"\n',
108 'node_modules/@demo/preset-row/package.json':JSON.stringify({name:'@demo/preset-row',type:'module',exports:'./index.mjs'}),
109 'node_modules/@demo/preset-row/index.mjs':authoredRow('B'),
110 }
111 const input=request(cfg,contents),review=await reviewAgentPresets(input);review.top=input.composition
112 const [a,b]=installedEntries(t,review,contents)
113 const first=await activate(host,'raw-presets',a.path,{scope:a})
114 assert.equal(first.result.status,'ok',first.result.diagnostic+' '+JSON.stringify(host.logs)+' '+host.stderr)
115 const second=await host.call('ext/activate',{owner:first.ref,plugin_name:'raw-presets',entry:b,scope:b,config:{}})
116 assert.equal(second.status,'ok',second.diagnostic)
117 const registrations=host.registry.filter(r=>r.op==='register')
118 for(const scope of [a,b])assert.deepEqual(new Set(registrations.filter(r=>r.scope.path===scope.path).map(r=>r.kind)),new Set(['tool','command','hook','prompt_section','skill_root']))
119 const handle=(scope,kind)=>registrations.find(r=>r.scope.path===scope.path && r.kind===kind).handle
120 const call=scope=>host.call('tool/call',{handle:handle(scope,'tool'),input:{},call_id:scope.path,deadline_ms:5000})
121 assert.match(JSON.stringify(await call(a)),/A:a/);assert.match(JSON.stringify(await call(b)),/B:b/)
122 assert.deepEqual(await host.call('command/run',{handle:handle(b,'command'),command_id:'preset-command',raw_input:'',deadline_ms:5000}),{kind:'success',text:'B:b'})
123 assert.deepEqual(await host.call('hook/evaluate',{handle:handle(b,'hook'),event:'tools/pre-execute',deadline_ms:5000,payload:{name:'read',call_id:'preset-hook',input:{},mode:'Agent',workspace:'/workspace',model:'fixture'}}),{kind:'annotate',text:'B:b'})
124 assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref,entry:a}),{disposed:true,leaked:[]})
125 await assert.rejects(call(a),/not live/);assert.match(JSON.stringify(await call(b)),/B:b/)
126 for(const row of registrations.filter(r=>r.scope.path===a.path)) assert.ok(host.registry.some(r=>r.op==='unregister' && r.handle===row.handle))
127 for(const row of registrations.filter(r=>r.scope.path===b.path)) assert.ok(!host.registry.some(r=>r.op==='unregister' && r.handle===row.handle))
128 assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref}),{disposed:true,leaked:[]})
129 })
130
131
132 test('absent raw default stays visible and requires explicit selection before real host mount',async t=>{
133 const noDefault={...cfg};delete noDefault.default
134 const contents={'presets/a/agent.cordis.yml':'- name: ./row.mjs\n','presets/a/row.mjs':authoredRow('A'),'presets/b/agent.cordis.yml':empty}
135 const input=request(noDefault,contents),review=await reviewAgentPresets(input);review.top=input.composition
136 assert.equal(review.catalog.default,undefined)
137 assert.ok(review.catalog.presets.every(row=>row.is_default===false))
138 const [a]=installedEntries(t,review,contents)
139 // Core stages a whole reviewed bundle before importing any of its entries.
140 // Creating a sibling after an import also hits Bun's directory resolver cache.
141 const root=dirname(a.path),bad=join(root,'unselected.mjs')
142 const source="import {mountReviewedPreset} from '@codewhale/dsh-composition';import data from './a.json' with {type:'json'};export async function apply(ctx){await mountReviewedPreset(ctx,new URL('../../source/',import.meta.url).href,data.composition,data.catalog,undefined)}"
143 writeFileSync(bad,source)
144 const host=await startHost();t.after(()=>host.stop())
145 const first=await activate(host,'absent-default',a.path,{scope:a})
146 assert.equal(first.result.status,'ok',first.result.diagnostic+' '+host.stderr)
147 const tool=host.registry.find(row=>row.op==='register' && row.kind==='tool')
148 assert.match(JSON.stringify(await host.call('tool/call',{handle:tool.handle,input:{},call_id:'explicit',deadline_ms:5000})),/A:a/)
149 // Unselected generated mount is an error, never a first-healthy fallback.
150 const refused=await activate(host,'missing-selected',bad,{scope:{path:bad,sha256:hash(source)}})
151 assert.equal(refused.result.status,'failed')
152 assert.match(refused.result.diagnostic,/explicit selection is required/)
153 })
154
155
156 test('actual installed source counterparts mount default and explicitly selected no-default entries',async t=>{
157 const host=await startHost();t.after(()=>host.stop())
158 for(const suffix of ['', '-no-default']) {
159 const root=new URL(`../../tests/fixtures/extension_host/raw-agent-presets${suffix}/`,import.meta.url)
160 const catalog=JSON.parse(readFileSync(new URL('native/presets.json',root),'utf8'))
161 assert.equal(catalog.default,suffix?undefined:'a')
162 const row=catalog.presets.find(p=>p.id==='b'),entry=fileURLToPath(new URL(row.entry.path,root))
163 assert.equal(hash(readFileSync(entry)),row.entry.sha256)
164 const admitted=await activate(host,'counterpart'+suffix,entry,{scope:{path:entry,sha256:row.entry.sha256}})
165 assert.equal(admitted.result.status,'ok',admitted.result.diagnostic+' '+host.stderr)
166 const tool=host.registry.findLast(r=>r.op==='register' && r.kind==='tool')
167 assert.match(JSON.stringify(await host.call('tool/call',{handle:tool.handle,input:{},call_id:'fixed'+suffix,deadline_ms:5000})),/B:b/)
168 assert.deepEqual(await host.call('ext/deactivate',{owner:admitted.ref}),{disposed:true,leaked:[]})
169 }
170 })
171
172
173 test('the existing upstream patch authority retains exact skipped operation identities and anonymous groups',()=>{
174 const first='- insert:\n - {id: group, group: true, config: []}\n - {id: docs-entry, name: portable}\n'
175 const second='- {id: missing-group, insert: []}\n- {disabled: true}\n- {id: missing-row, disabled: true}\n- {id: docs-entry, name: wrong-package, disabled: true}\n- id: group\n insert:\n - {id: child, name: ./row.mjs}\n'
176 const spec={version:1,layers:[{path:'first.yml',source:first,sha256:hash(first)},{path:'overlay.yml',source:second,sha256:hash(second)}],modules:[],files:{}}
177 const result=spawnSync(process.execPath,[fileURLToPath(new URL('../dist/dsh-composition-review.mjs',import.meta.url))],{input:JSON.stringify(spec),encoding:'utf8',timeout:5000})
178 assert.equal(result.status,0,result.stderr)
179 const reviewed=JSON.parse(result.stdout)
180 assert.deepEqual(reviewed.skipped.map(row=>[row.row??null,row.layer,row.patch]),[['missing-group','overlay.yml',1],[null,'overlay.yml',2],['missing-row','overlay.yml',3],['docs-entry','overlay.yml',4]])
181 assert.equal(reviewed.entries[1].disabled,undefined)
182 assert.equal(reviewed.entries[0].name,undefined,'raw patch comparison keeps its original anonymous identity')
183 assert.equal(reviewed.entries[0].config[0].name,'./row.mjs')
184 })
185
186 test('roster inventory uses real selected fiber facts and unmounted conditional rows without another graph',async t=>{
187 const host=await startHost();t.after(()=>host.stop())
188 const source=`export const inject=['tools','agentPresets'];export function apply(ctx){ctx.tools.register({name:'preset_inspect',description:'readonly roster',parameters:{type:'object',properties:{}},execute:async()=>({roster:await ctx.agentPresets.remoteExportList(),inventory:await ctx.agentPresets.compositionInventory(),selected:ctx.agentPresets.composedPreset()})})}`
189 const contents={'presets/a/agent.cordis.yml':'- name: ./row.mjs\n','presets/a/row.mjs':source,'presets/b/agent.cordis.yml':"- name: missing-conditional\n disabled: !!js 'true'\n"}
190 const input=request(cfg,contents)
191 const top=JSON.stringify([{insert:[{id:'anonymous',group:true,config:JSON.parse(input.composition.layers[0].source)[0].insert}]}])
192 input.composition.layers[0]={path:'bundle.json',source:top,sha256:hash(top)}
193 const review=await reviewAgentPresets(input);review.top=input.composition
194 const [a]=installedEntries(t,review,contents)
195 const mounted=await activate(host,'inventory-roster',a.path,{scope:a})
196 assert.equal(mounted.result.status,'ok',mounted.result.diagnostic+' '+host.stderr+' '+JSON.stringify(host.logs))
197 const tool=host.registry.find(r=>r.op==='register' && r.kind==='tool')
198 const result=await host.call('tool/call',{handle:tool.handle,input:{},call_id:'inventory',deadline_ms:5000})
199 assert.equal(result.structured.selected,'a')
200 assert.equal(result.structured.roster.authorable,false)
201 assert.equal(result.structured.roster.defaultId,'a')
202 assert.equal(result.structured.inventory[0].isDefault,true)
203 assert.equal(result.structured.inventory[0].rows[0].enabled,true)
204 assert.ok(Object.hasOwn(result.structured.inventory[0].rows[0],'fiberState'))
205 assert.equal(result.structured.inventory[1].rows[0].enabled,'conditional')
206 assert.equal(Object.hasOwn(result.structured.inventory[1].rows[0],'fiberState'),false)
207 assert.deepEqual(await host.call('ext/deactivate',{owner:mounted.ref}),{disposed:true,leaked:[]})
208 })
209
210
211 test('real raw persona rows admit stock additive templates under exact sibling scopes', async t => {
212 const contents = {
213 'presets/a/agent.cordis.yml': '- name: "@deepseek-ai/dsh-persona"\n config: {prefix: "You are {{model}}.", suffix: "Work in {{cwd}}."}\n',
214 'presets/b/agent.cordis.yml': '- name: "@deepseek-ai/dsh-persona"\n config: {prefix: "B {{model}}", suffix: "{{cwd}}"}\n',
215 'presets/replacement/agent.cordis.yml': '- name: "@deepseek-ai/dsh-persona"\n config: {prefix: replace, complete: true}\n',
216 'presets/suppression/agent.cordis.yml': '- name: "@deepseek-ai/dsh-persona"\n config: {prefix: suppress, includeRuntimeContext: false}\n',
217 'presets/unknown/agent.cordis.yml': '- name: "@deepseek-ai/dsh-persona"\n config: {prefix: "{{env}}"}\n',
218 }
219 const input = request(cfg, contents), review = await reviewAgentPresets(input); review.top = input.composition
220 assert.deepEqual(review.presets.map(row=>row.metadata.id), ['a','b'])
221 assert.match(review.catalog.presets.find(row=>row.id==='replacement').broken,/Core prompt/)
222 assert.match(review.catalog.presets.find(row=>row.id==='suppression').broken,/Core prompt/)
223 assert.match(review.catalog.presets.find(row=>row.id==='unknown').broken,/unknown Core prompt variable/)
224 assert.ok(review.presets.every(row=>row.composition.modules.length===0),'fixed bridge requires no ambient persona package')
225 const [a,b] = installedEntries(t, review, contents)
226 const host = await startHost(); t.after(()=>host.stop())
227 const first = await activate(host,'raw-personas',a.path,{scope:a})
228 assert.equal(first.result.status,'ok',first.result.diagnostic+' '+host.stderr)
229 const second = await host.call('ext/activate',{owner:first.ref,plugin_name:'raw-personas',entry:b,scope:b,config:{}})
230 assert.equal(second.status,'ok',second.diagnostic)
231 const rows = host.registry.filter(row=>row.op==='register' && row.kind==='prompt_template')
232 assert.equal(rows.length,4)
233 for (const scope of [a,b]) assert.deepEqual(rows.filter(row=>row.scope.path===scope.path).map(row=>row.spec.name),['persona-prefix','persona-suffix'])
234 assert.ok(rows.every(row=>row.spec.description.includes('{{')),'only Core may expand actual turn facts')
235 assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref,entry:a}),{disposed:true,leaked:[]})
236 for (const row of rows) assert.equal(host.registry.some(event=>event.op==='unregister' && event.handle===row.handle),row.scope.path===a.path)
237 assert.deepEqual(await host.call('ext/deactivate',{owner:first.ref}),{disposed:true,leaked:[]})
238 })
239
239 lines Plain Text