dualEnvironment.live.test.ts 5.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. // @vitest-environment node
  2. import {it,expect,vi} from 'vitest'
  3. import {createServer} from 'node:http'
  4. import {DatabaseSync} from 'node:sqlite'
  5. import {mkdtemp,writeFile,readFile,rm} from 'node:fs/promises'
  6. import {join,resolve} from 'node:path'
  7. import {tmpdir} from 'node:os'
  8. import {randomUUID} from 'node:crypto'
  9. import {ComputerRuntime,ComputerGateway} from '../../src/runner/worker/gateway'
  10. it.skipIf(!process.env.YAOYAO_HERMES_PYTHON||!process.env.YAOYAO_COMPUTER_IMAGE)('uses host and VM tools in one real Worker turn and copies binary files in both directions',async()=>{
  11. const home=await mkdtemp(join(tmpdir(),'yaoyao-dual-live-')),db=new DatabaseSync(':memory:'),proof=randomUUID()
  12. const source=join(home,'host-source.bin'),returned=join(home,'returned.bin'),bytes=Buffer.alloc(9*1024*1024,0xa5)
  13. await writeFile(source,bytes);await writeFile(join(home,'host-proof.txt'),proof)
  14. const requests:any[]=[],toolResults:any[]=[]
  15. const steps=[
  16. ['host_read_file',{path:'host-proof.txt'}],
  17. ['host_shell',{command:'uname -s; pwd'}],
  18. ['computer_copy_file',{from:'host',source:'host-source.bin',destination:'import.bin'}],
  19. ['computer_shell',{command:`uname -s; test ! -e '${source}'; wc -c < import.bin`}],
  20. ['computer_copy_file',{from:'vm',source:'import.bin',destination:'returned.bin'}],
  21. ] as const
  22. const server=createServer(async(req,res)=>{
  23. if(req.method==='GET'){res.setHeader('content-type','application/json');res.end(JSON.stringify({data:[{id:'fixture-model',context_length:65536}]}));return}
  24. let body='';for await(const chunk of req)body+=chunk;const input=JSON.parse(body);requests.push(input)
  25. const results=input.messages.filter((m:any)=>m.role==='tool');toolResults.push(...results)
  26. const step=steps[results.length]
  27. const message=step?{role:'assistant',content:null,tool_calls:[{id:'dual-'+results.length,type:'function',function:{name:step[0],arguments:JSON.stringify(step[1])}}]}:{role:'assistant',content:'双环境完成'}
  28. if(input.stream){res.setHeader('content-type','text/event-stream');res.end('data: '+JSON.stringify({id:randomUUID(),object:'chat.completion.chunk',model:'fixture-model',choices:[{index:0,delta:{...message,...(message.tool_calls?{tool_calls:message.tool_calls.map((t:any,index:number)=>({...t,index}))}:{})},finish_reason:step?'tool_calls':'stop'}]})+'\n\ndata: [DONE]\n\n')}
  29. else{res.setHeader('content-type','application/json');res.end(JSON.stringify({id:randomUUID(),object:'chat.completion',model:'fixture-model',choices:[{index:0,message,finish_reason:step?'tool_calls':'stop'}],usage:{prompt_tokens:10,completion_tokens:10,total_tokens:20}}))}
  30. })
  31. await new Promise<void>(done=>server.listen(0,'127.0.0.1',done))
  32. const base=`http://127.0.0.1:${(server.address() as {port:number}).port}`
  33. const runtime=new ComputerRuntime(db,{protocol:1,runnerId:randomUUID(),serverURL:base,hermesURL:base,token:'fixture',allowedProfiles:['default'],artifactRoots:[],computers:{runtime:'docker',imageId:process.env.YAOYAO_COMPUTER_IMAGE!,python:process.env.YAOYAO_HERMES_PYTHON!,hermesSource:process.env.YAOYAO_HERMES_SOURCE!,hermesHome:home,network:'none'}},home,resolve('src/runner/worker/hermes_worker.py'))
  34. const id=randomUUID(),gateway=new ComputerGateway(runtime,{agentId:id,environmentId:id,ownerKey:'dual-fixture',hostAccess:true},randomUUID(),async()=>{},async()=>({}))
  35. const completed=new Promise<any>((done,reject)=>{gateway.onEvent=frame=>{if(frame.type==='message.complete')frame.payload.status==='complete'?done(frame.payload):reject(new Error(JSON.stringify(frame)))};gateway.onDisconnect=()=>reject(new Error('Worker disconnected'))})
  36. try{
  37. await runtime.ready
  38. vi.spyOn(runtime,'resolve').mockResolvedValue({type:'resolved',cwd:'/home/cua/workspace',configuredCwd:home,model:{model:'fixture-model',provider:'custom',api_mode:'chat_completions',base_url:base+'/v1',api_key:'fixture'}})
  39. await gateway.connect();const session=await gateway.rpc('session.create',{profile:'default'})
  40. await gateway.rpc('prompt.submit',{session_id:session.session_id,text:'依次执行本机读取、本机命令、文件复制、虚拟机命令,并把结果复制回本机。'})
  41. expect((await completed).text).toBe('双环境完成')
  42. expect(await readFile(returned)).toEqual(bytes)
  43. expect(toolResults.some(m=>String(m.content).includes(proof))).toBe(true)
  44. expect(toolResults.some(m=>String(m.content).includes('Linux')&&String(m.content).includes(String(bytes.length)))).toBe(true)
  45. expect(toolResults.some(m=>String(m.content).includes(process.platform==='darwin'?'Darwin':'Linux')&&String(m.content).includes(home))).toBe(true)
  46. for(const req of requests){expect(req.tools.map((t:any)=>t.function.name)).toEqual(expect.arrayContaining(['host_shell','host_read_file','computer_shell','computer_copy_file']));expect(JSON.stringify(req.messages.filter((m:any)=>m.role==='system'))).toContain('separate files')}
  47. }finally{
  48. await gateway.close();await runtime.controls.close();await runtime.pool.close();db.close();vi.restoreAllMocks()
  49. await new Promise<void>(done=>{server.close(()=>done());server.closeAllConnections()});await rm(home,{recursive:true,force:true})
  50. }
  51. },120000)