| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 |
- // @vitest-environment node
- import {afterEach,expect,it,vi} from 'vitest'
- import {createServer,type IncomingMessage,type ServerResponse,type Server} from 'node:http'
- import {mkdtemp,mkdir,writeFile,rm} from 'node:fs/promises'
- import {tmpdir} from 'node:os'
- import {join,resolve} from 'node:path'
- import {randomUUID} from 'node:crypto'
- import {HermesWorkerProcess} from '../../src/runner/worker/process'
- afterEach(()=>vi.unstubAllEnvs())
- const enabled=!!process.env.YAOYAO_HERMES_PYTHON&&!!process.env.YAOYAO_HERMES_SOURCE
- const listen=(server:Server)=>new Promise<number>(resolve=>server.listen(0,'127.0.0.1',()=>resolve((server.address() as {port:number}).port)))
- const close=(server:Server)=>new Promise<void>(resolve=>{server.close(()=>resolve());server.closeAllConnections()})
- it.skipIf(!enabled).each(['uppercase','lowercase','no-proxy','unconfigured'] as const)('uses the actual Hermes loop with Profile proxy policy: %s',async mode=>{
- const root=await mkdtemp(join(tmpdir(),'yaoyao-worker-proxy-live-')),hermesHome=join(root,'hermes'),workerHome=join(root,'worker')
- const profile=mode==='uppercase'?'default':'writer',profileHome=profile==='default'?hermesHome:join(hermesHome,'profiles',profile)
- await mkdir(profileHome,{recursive:true})
- const proxyRequests:Array<{url:string;auth?:string}>=[],directRequests:string[]=[],modelRequests:any[]=[]
- const reply=async(req:IncomingMessage,res:ServerResponse)=>{
- const url=new URL(req.url!,'http://fixture')
- if(req.method==='GET'&&url.pathname==='/v1/models'){
- res.setHeader('Content-Type','application/json');res.end(JSON.stringify({data:[{id:'fixture-model',object:'model',context_length:65536}]}));return
- }
- if(req.method!=='POST'||url.pathname!=='/v1/chat/completions'){res.statusCode=404;res.end('{}');return}
- let body='';for await(const chunk of req)body+=chunk
- const input=JSON.parse(body);modelRequests.push(input)
- if(req.headers.authorization!=='Bearer fixture-model-key'){res.statusCode=401;res.end('{}');return}
- const message={role:'assistant',content:'代理链路已完成完整回复。'}
- if(input.stream){
- res.setHeader('Content-Type','text/event-stream')
- res.end('data: '+JSON.stringify({id:'fixture-response',object:'chat.completion.chunk',model:'fixture-model',choices:[{index:0,delta:message,finish_reason:'stop'}]})+'\n\ndata: [DONE]\n\n')
- }else{
- res.setHeader('Content-Type','application/json')
- res.end(JSON.stringify({id:'fixture-response',object:'chat.completion',model:'fixture-model',choices:[{index:0,message,finish_reason:'stop'}],usage:{prompt_tokens:10,completion_tokens:10,total_tokens:20}}))
- }
- }
- // The HTTP proxy terminates a fixture-only origin. The .invalid name cannot
- // resolve directly, so a successful model response proves proxy traversal.
- const proxy=createServer((req,res)=>{
- proxyRequests.push({url:req.url!,auth:req.headers['proxy-authorization']})
- if(!req.url?.startsWith('http://worker-proxy.invalid/')){res.statusCode=502;res.end('{}');return}
- void reply(req,res).catch(()=>{res.statusCode=500;res.end('{}')})
- })
- const direct=createServer((req,res)=>{directRequests.push(req.url!);void reply(req,res).catch(()=>{res.statusCode=500;res.end('{}')})})
- const workers:HermesWorkerProcess[]=[]
- let diagnostics=''
- try{
- const [proxyPort,directPort]=await Promise.all([listen(proxy),listen(direct)])
- const proxyURL=`http://fixture-user:fixture-proxy-password@127.0.0.1:${proxyPort}`
- const proxied=mode==='uppercase'||mode==='lowercase'
- const baseURL=proxied?'http://worker-proxy.invalid/v1':`http://127.0.0.1:${directPort}/v1`
- // A Runner-level proxy must neither override a Profile nor become a fallback.
- for(const key of ['HTTP_PROXY','HTTPS_PROXY','ALL_PROXY','http_proxy','https_proxy','all_proxy'])vi.stubEnv(key,proxyURL)
- for(const key of ['NO_PROXY','no_proxy'])vi.stubEnv(key,'')
- await writeFile(join(profileHome,'config.yaml'),`model:\n provider: custom\n default: fixture-model\n base_url: ${baseURL}\n api_key: \${WORKER_FIXTURE_KEY}\n api_mode: chat_completions\nterminal:\n cwd: /home/cua/workspace\n`,{mode:0o600})
- const proxyVars=mode==='uppercase'?`HTTP_PROXY=\${FIXTURE_PROXY_URL}\nHTTPS_PROXY=\${FIXTURE_PROXY_URL}\nNO_PROXY=\n`:mode==='lowercase'?`http_proxy=\${FIXTURE_PROXY_URL}\nhttps_proxy=\${FIXTURE_PROXY_URL}\nno_proxy=\n`:mode==='no-proxy'?`HTTP_PROXY=\${FIXTURE_PROXY_URL}\nHTTPS_PROXY=\${FIXTURE_PROXY_URL}\nNO_PROXY=127.0.0.1,localhost\n`:''
- await writeFile(join(profileHome,'.env'),`WORKER_FIXTURE_KEY=fixture-model-key\nFIXTURE_PROXY_URL=${proxyURL}\nUNRELATED_SECRET=fixture-unrelated-secret\n${proxyVars}`,{mode:0o600})
- const resolver=new HermesWorkerProcess(process.env.YAOYAO_HERMES_PYTHON!,resolve('src/runner/worker/hermes_worker.py'),{mode:'resolve',profile,hermesHome,hermesSource:process.env.YAOYAO_HERMES_SOURCE})
- workers.push(resolver)
- const resolved=await resolver.wait('resolved');await resolver.close()
- expect(resolved.proxyEnv??{}).not.toHaveProperty('UNRELATED_SECRET')
- expect(resolved.proxyEnv??{}).not.toHaveProperty('FIXTURE_PROXY_URL')
- if(mode==='unconfigured')expect(resolved).not.toHaveProperty('proxyEnv')
- const worker=new HermesWorkerProcess(process.env.YAOYAO_HERMES_PYTHON!,resolve('src/runner/worker/hermes_worker.py'),{diagnostic:true,mode:'run',home:workerHome,hermesSource:process.env.YAOYAO_HERMES_SOURCE,model:resolved.model,proxyEnv:resolved.proxyEnv,sessionId:randomUUID(),taskId:randomUUID(),cwd:resolved.cwd,tools:[],prompt:'返回测试回复。',history:[]},text=>{diagnostics+=text})
- workers.push(worker)
- const frames:unknown[]=[];worker.onEvent=frame=>frames.push(frame)
- const complete=await worker.wait('complete',45000).catch(error=>{throw new Error(`${String(error)}\n${diagnostics}`)})
- expect(complete).toMatchObject({text:'代理链路已完成完整回复。',completed:true})
- expect(modelRequests.length).toBeGreaterThan(0)
- if(proxied){
- expect(proxyRequests.some(request=>request.url==='http://worker-proxy.invalid/v1/chat/completions')).toBe(true)
- expect(proxyRequests.find(request=>request.url.endsWith('/chat/completions'))?.auth).toBe('Basic '+Buffer.from('fixture-user:fixture-proxy-password').toString('base64'))
- expect(directRequests).toEqual([])
- }else{
- expect(directRequests).toContain('/v1/chat/completions')
- expect(proxyRequests).toEqual([])
- }
- expect(JSON.stringify(frames)+diagnostics).not.toMatch(/fixture-proxy-password|fixture-unrelated-secret|proxyEnv/)
- }finally{
- await Promise.all(workers.map(worker=>worker.close()))
- await Promise.all([close(proxy),close(direct)])
- await rm(root,{recursive:true,force:true})
- }
- },60000)
|