返回 CodeWhale
web-adapters.test.mjs
根目录 / crates / tui / extension-host / test / web-adapters.test.mjs
1 import test from 'node:test'
2 import assert from 'node:assert/strict'
3 import { dirname, join } from 'node:path'
4 import { readFileSync } from 'node:fs'
5 import { createHash } from 'node:crypto'
6 import { transformStockSnapshot } from '../dist/stock-adapters.mjs'
7 import { createHarnessModule } from '../dist/builtin/harness.mjs'
8 import { startHost, BUNDLE } from './harness.mjs'
9 const transform=(operation,input)=>transformStockSnapshot({kind:'stock_adapter',operation,input}).result.metadata
10 const decode=(backend,parsed,number_facts={},max_results=10)=>transform('web_provider',{backend,parsed,number_facts,max_results})
11 const a={title:' A ',url:' web-url-0 ',content:'\u0085meaningful\u0085'}, b={title:' B ',url:'web-url-1',snippet:' fallback '}
12 for(const [backend,parsed,expected] of [
13 ['tavily',{results:[a,b]},{title:'A',url:'web-url-0',snippet:'meaningful'}],
14 ['firecrawl',{data:{web:[{...a,description:' summary '}]}},{title:'A',url:'web-url-0',snippet:'summary'}],
15 ['metaso',{webpages:[{...a,link:a.url,snippet:' snippet '}],code:0},{title:'A',url:'web-url-0',snippet:'snippet'}],
16 ['bocha',{data:{webPages:{value:[{name:' A ',url:a.url,summary:' summary '}]}}},{title:'A',url:'web-url-0',snippet:'summary'}],
17 ['baidu',{references:[{name:' A ',link:a.url,content:' content '}]},{title:'A',url:'web-url-0',snippet:'content'}],
18 ['searxng',{results:[a,b]},{title:'A',url:'web-url-0',snippet:'meaningful'}],
19 ['sofya',{results:[a]},{title:' A ',url:' web-url-0 ',snippet:'meaningful'}],
20 ['serply',{results:[{...a,link:a.url,description:' description '}]},{title:' A ',url:' web-url-0 ',snippet:'description'}],
21 ['volcengine',{output:[{type:'message',content:[{text:'```json\n'+JSON.stringify({results:[{...a,snippet:' source '}]})+'\n```'}]}]},{title:'A',url:'web-url-0',snippet:'source'}],
22 ])test(`${backend} exact aliases, whitespace and candidate order`,()=>assert.deepEqual(decode(backend,parsed).entries[0],expected))
23 test('present null/wrong fields suppress fallback only for the correct providers',()=>{
24 assert.deepEqual(decode('bocha',{pages:[{name:null,title:'fallback',url:'u0'},{name:'B',url:'u2',summary:null,snippet:'ignored'}]}).entries,[{title:'B',url:'u2'}])
25 assert.deepEqual(decode('baidu',{references:[{title:'A',url:'u0',content:4,snippet:'ignored'}]}).entries,[{title:'A',url:'u0'}])
26 assert.deepEqual(decode('tavily',{results:[{title:'A',url:'u0',content:4,snippet:'used'}]}).entries,[{title:'A',url:'u0',snippet:'used'}])
27 assert.deepEqual(decode('firecrawl',{data:{web:null},results:[a]}).entries,[])
28 })
29 test('Core integer facts distinguish float/string from exact signed integer API codes',()=>{
30 for(const i64 of [null,undefined])assert.equal(decode('metaso',{code:2005},{'/code':{i64}}).error,null)
31 assert.match(decode('metaso',{code:2005},{'/code':{i64:'2005'}}).error,/API key rejected/)
32 assert.match(decode('metaso',{code:3003},{'/code':{i64:'3003'}}).error,/daily search limit/)
33 assert.equal(decode('bocha',{code:200},{'/code':{i64:'200'}}).error,null)
34 assert.equal(decode('baidu',{error_code:null,code:9},{'/error_code':{i64:null},'/code':{i64:'9'}}).error,null)
35 })
36 test('stable score sort includes positive versus negative zero',()=>{
37 const parsed={results:['negative','zero','high','equal'].map((title,i)=>({title,url:`u${i}`}))}
38 const facts=Object.fromEntries(['-0','0','1e+300','1e+300'].map((score,i)=>[`/results/${i}/score`,{score}]))
39 assert.deepEqual(decode('searxng',parsed,facts,3).entries.map(value=>value.title),['high','equal','zero'])
40 })
41 test('Firecrawl scalar cap keeps astral glyphs and Rust trim keeps BOM',()=>{
42 assert.equal([...decode('firecrawl',{data:[{title:'\ufeffA',url:'u',description:'😀'.repeat(1001)}]}).entries[0].snippet].length,1000)
43 assert.equal(decode('tavily',{results:[{title:'\ufeff',url:'u'}]}).entries[0].title,'\ufeff')
44 })
45 test('Volcengine missing text, null error, latest message and invalid inner JSON are distinct',()=>{
46 assert.match(decode('volcengine',{error:null}).error,/code unknown: no details/)
47 assert.match(decode('volcengine',{output:[{type:'reasoning',content:[{text:'{}'}]}]}).error,/no output text/)
48 assert.deepEqual(decode('volcengine',{output:[{type:'message',content:[{text:'not JSON'}]}]}).entries,[])
49 assert.deepEqual(decode('volcengine',{output:[{type:'message',content:[{text:'{"results":[{"title":"old","url":"u"}]}'}]},{type:'message',content:[{text:'{"results":[]}'}]}]}).entries,[])
50 })
51 test('all eleven providers use keyless plans without endpoint, model or credential authority',()=>{
52 for(const backend of ['firecrawl','tavily','bocha','metaso','sofya','baidu','volcengine','serply','searxng','bing','duckduckgo']){
53 const plan=transform('web_request',{backend,query:'authorized query',max_results:5,filters:transform('web_filters',{recency_days:10,locale:' de_DE '}),locale:'de_DE'})
54 assert.equal(plan.kind,'web_request');assert.equal(JSON.stringify(plan).includes('api_key'),false);assert.equal(JSON.stringify(plan).includes('Authorization'),false)
55 }
56 assert.deepEqual(transform('web_request',{backend:'serply',query:'q',max_results:5,filters:transform('web_filters',{locale:'zh-CN',recency_days:null})}),{kind:'web_request',payload:null,pairs:[['q','q'],['num','5'],['hl','zh'],['gl','cn']]})
57 })
58 test('filter rounding uses exact Rust whitespace and language/region rules',()=>{
59 for(const [recency_days,window]of [[1,'day'],[2,'week'],[7,'week'],[8,'month'],[31,'month'],[32,'year'],[3650,'year']])assert.equal(transform('web_filters',{recency_days,locale:null}).window,window)
60 assert.deepEqual(transform('web_filters',{recency_days:null,locale:'\u0085ZH_cn\u0085'}),{kind:'web_filters',window:null,language:'zh',region:'CN'})
61 assert.equal(transform('web_filters',{locale:'\ufeffen-US'}).language,null)
62 })
63 test('receipt composes ignored knobs around mandatory Core domain facts and opaque note presence',()=>{
64 const got=transform('web_finalize',{requested:{recency:true,domains:true,locale:true},capabilities:{max_results:'supported',recency:'unsupported',locale:'supported'},count:2,degraded:[{kind:'answer_cut_by_provider'}],domain_extra:[{kind:'post_filtered',knob:'domains'}],has_note:true})
65 assert.deepEqual(got.honored,{max_results:true,recency:false,domains:true,locale:true});assert.equal(got.prefix,'Found 2 result(s). ');assert.match(got.suffix,/incomplete/)
66 assert.deepEqual(got.degraded,[{kind:'answer_cut_by_provider'},{kind:'knob_ignored',knob:'recency'},{kind:'post_filtered',knob:'domains'}])
67 })
68 test('malformed and oversized proposals refuse rather than return partial/empty results',()=>{
69 for(const [op,value]of [['web_provider',{backend:'unknown',parsed:{},max_results:5,number_facts:{}}],['web_images',{max_results:5,parsed:{results:[{image:'u',width:-1}]}}],['web_filters',{recency_days:3651}],['web_entries',{entries:[{title:4,url:'u'}]}]])assert.throws(()=>transform(op,value))
70 assert.throws(()=>decode('tavily',{results:[{title:'x'.repeat(1024*1024),url:'u'}]}),/1 MiB/)
71 })
72 test('image candidates skip blank images before Core source-domain/count filtering',()=>{
73 assert.deepEqual(transform('web_images',{max_results:1,parsed:{results:[{image:'\u0085'},{image:'opaque-image',width:4294967295,title:'A',url:'opaque-page'},{image:'opaque-second'}]}}),{kind:'web_images',max_results:1,entries:[{image:'opaque-image',title:'A',url:'opaque-page',width:4294967295},{image:'opaque-second'}]})
74 })
75 test('runner cancellation/revocation refuses late web completion with one redemption',async()=>{
76 const owner={plugin_id:'host:harness',generation:4,owner_token:'reviewed-builtin'};let calls=0,finish
77 const runner=createHarnessModule({request(){calls++;return new Promise(resolve=>finish=resolve)}},owner);const stop=new AbortController()
78 const pending=runner.run({owner,execution_id:'web',ticket:'opaque-operation',deadline_ms:1000},stop.signal)
79 stop.abort();await assert.rejects(pending,/cancelled/);finish({kind:'stock_adapter',operation:'web_filters',input:{recency_days:null,locale:null}});assert.equal(calls,1);await runner.dispose()
80 await assert.rejects(runner.run({owner,execution_id:'again',ticket:'opaque-operation',deadline_ms:1000},new AbortController().signal),/stale/)
81 })
82 test('real Builtin Host wire runs the actual web adapter with fake Core and one opaque grant',async t=>{
83 const host=await startHost({tier:'builtin'});t.after(()=>host.stop());const owner={plugin_id:'host:harness',generation:4,owner_token:'fixture-web'}
84 const entry=join(dirname(BUNDLE),'builtin','harness.mjs'),sha256=createHash('sha256').update(readFileSync(entry)).digest('hex')
85 assert.equal((await host.call('ext/activate',{owner,plugin_name:'Pinned web adapter',entry:{path:entry,sha256},config:{}})).status,'ok')
86 const redeemed=host.waitFor(message=>message.method==='exec/redeem');const pending=host.call('harness/run',{owner,execution_id:'wire-web',ticket:'opaque-operation',deadline_ms:1000});const message=await redeemed
87 assert.deepEqual(message.params,{owner,execution_id:'wire-web',ticket:'opaque-operation'})
88 host.send({jsonrpc:'2.0',id:message.id,result:{kind:'stock_adapter',operation:'web_provider',input:{backend:'tavily',max_results:1,parsed:{results:[a]},number_facts:{}}}})
89 assert.deepEqual((await pending).result.metadata,decode('tavily',{results:[a]},{},1));assert.deepEqual(await host.call('ext/deactivate',{owner}),{disposed:true,leaked:[]})
90 })
91
91 lines Plain Text