workerProxy.test.ts 6.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // @vitest-environment node
  2. import {expect,it,vi} from 'vitest'
  3. import {DatabaseSync} from 'node:sqlite'
  4. import {mkdtemp,writeFile,rm} from 'node:fs/promises'
  5. import {tmpdir} from 'node:os'
  6. import {join} from 'node:path'
  7. import {randomUUID} from 'node:crypto'
  8. import {ComputerGateway,ComputerRuntime} from '../../src/runner/worker/gateway'
  9. import type {WorkerProxyEnvironment} from '../../src/runner/worker/process'
  10. it('keeps proxy settings private, reuses them after takeover, and refreshes them when resuming a session',async()=>{
  11. const home=await mkdtemp(join(tmpdir(),'yaoyao-worker-proxy-')),db=new DatabaseSync(':memory:'),script=join(home,'probe.py')
  12. // Exercise the real Gateway, process launcher and lease lifecycle; replace
  13. // only the model loop and the VM provider. The probe stays on the private pipe.
  14. await writeFile(script,`import json,os,sys
  15. boot=json.loads(sys.stdin.readline())
  16. print(json.dumps({"nonce":boot["nonce"],"type":"probe","env":{k:v for k,v in os.environ.items() if k.lower().endswith("_proxy")},"boot":boot}),flush=True)
  17. for line in sys.stdin:
  18. if json.loads(line).get("type")=="interrupt": break
  19. `)
  20. const runtime=new ComputerRuntime(db,{protocol:1,runnerId:randomUUID(),serverURL:'http://127.0.0.1',hermesURL:'http://127.0.0.1',token:'fixture',allowedProfiles:['default'],artifactRoots:[],computers:{runtime:'docker',imageId:'sha256:'+'a'.repeat(64),python:'python3',hermesSource:home,hermesHome:home,network:'none'}},home,script)
  21. const id=randomUUID(),meta={environmentId:id,agentId:id,ownerKey:'owner'},gateways:ComputerGateway[]=[],events:unknown[]=[]
  22. const first={HTTPS_PROXY:'http://user:first-private-proxy@127.0.0.1:7890',NO_PROXY:'localhost'}
  23. const second={https_proxy:'http://user:second-private-proxy@127.0.0.1:8890',no_proxy:'internal.test'}
  24. let current:WorkerProxyEnvironment|undefined=first
  25. let contextConfig={compression:{enabled:true,threshold:0.5}}
  26. const createGateway=()=>{
  27. const gateway=new ComputerGateway(runtime,meta,randomUUID(),async()=>{},async()=>({}))
  28. gateway.onEvent=frame=>events.push(frame);gateways.push(gateway);return gateway
  29. }
  30. const probe=(gateway:ComputerGateway)=>runtime.workers.get(gateway.workId)!.process.wait('probe',5000)
  31. try{
  32. await runtime.ready
  33. const resolver=vi.spyOn(runtime,'resolve').mockImplementation(async()=>({type:'resolved',cwd:'/home/cua/workspace',configuredCwd:'.',model:{provider:'custom',api_mode:'chat_completions',model:'fixture'},contextConfig:structuredClone(contextConfig),...(current?{proxyEnv:{...current}}:{})}))
  34. const state={id,containerId:'fixture',running:true,workspace:home,isolation:'container' as const}
  35. const ensure=vi.spyOn(runtime.provider,'ensure').mockResolvedValue(state)
  36. vi.spyOn(runtime.provider,'inspect').mockResolvedValue(state)
  37. vi.spyOn(runtime.provider,'stop').mockResolvedValue();vi.spyOn(runtime.provider,'remove').mockResolvedValue()
  38. const gateway=createGateway();await gateway.connect()
  39. const created=await gateway.rpc('session.create',{profile:'default'})
  40. await gateway.rpc('prompt.submit',{session_id:created.session_id,text:'first turn'})
  41. expect((await probe(gateway)).env).toEqual(first)
  42. const oldWorker=runtime.workers.get(gateway.workId)!.process
  43. current=second
  44. contextConfig={compression:{enabled:false,threshold:0.7}}
  45. await gateway.takeControl(randomUUID(),()=>{})
  46. await gateway.giveBack('continue')
  47. const continued=await probe(gateway)
  48. expect(continued.env).toEqual(first)
  49. expect(continued.boot.contextConfig).toEqual({compression:{enabled:true,threshold:0.5}})
  50. oldWorker.onEvent({type:'checkpoint',messages:[{role:'user',content:'stale worker history'}]})
  51. expect(JSON.stringify(runtime.session(created.session_id,meta,'default').history)).not.toContain('stale worker history')
  52. expect(continued.boot.history).toContainEqual({role:'user',content:'first turn'})
  53. expect(resolver).toHaveBeenCalledTimes(1)
  54. await gateway.close()
  55. const resumed=createGateway();await resumed.connect()
  56. expect((await resumed.rpc('session.resume',{profile:'default',session_id:created.session_id})).session_id).toBe(created.session_id)
  57. await resumed.rpc('prompt.submit',{session_id:created.session_id,text:'next turn'})
  58. const resumedProbe=await probe(resumed)
  59. expect(resumedProbe.env).toEqual(second)
  60. expect(resumedProbe.boot.contextConfig).toEqual(contextConfig)
  61. expect(resolver).toHaveBeenCalledTimes(2)
  62. await resumed.close()
  63. current=undefined
  64. const direct=createGateway();await direct.connect()
  65. await direct.rpc('session.resume',{profile:'default',session_id:created.session_id})
  66. await direct.rpc('prompt.submit',{session_id:created.session_id,text:'direct turn'})
  67. const frame=await probe(direct)
  68. expect(frame.env).toEqual({})
  69. expect(frame.boot.model).not.toHaveProperty('proxyEnv')
  70. expect(frame.boot).not.toHaveProperty('proxyEnv')
  71. expect(resolver).toHaveBeenCalledTimes(3)
  72. const persisted=db.prepare('SELECT value FROM computer_sessions').all()
  73. for(const value of [created,persisted,events,ensure.mock.calls])expect(JSON.stringify(value)).not.toMatch(/private-proxy|proxyEnv|HTTPS_PROXY|https_proxy/)
  74. }finally{
  75. await Promise.all(gateways.map(gateway=>gateway.close()))
  76. await runtime.controls.close();await runtime.pool.close();db.close()
  77. vi.restoreAllMocks();await rm(home,{recursive:true,force:true})
  78. }
  79. })
  80. it('extracts memory with an empty tool catalog and without acquiring or starting a computer', async () => {
  81. const home=await mkdtemp(join(tmpdir(),'yaoyao-memory-worker-')),db=new DatabaseSync(':memory:'),script=join(home,'memory.py')
  82. await writeFile(script,`import json,sys\nboot=json.loads(sys.stdin.readline())\nprint(json.dumps({"nonce":boot["nonce"],"type":"complete","completed":True,"interrupted":False,"text":json.dumps({"tools":boot["tools"],"prompt":boot["prompt"],"history":boot["history"]})}),flush=True)\n`)
  83. const runtime=new ComputerRuntime(db,{protocol:1,runnerId:randomUUID(),serverURL:'http://127.0.0.1',hermesURL:'http://127.0.0.1',token:'fixture',allowedProfiles:['default'],artifactRoots:[],computers:{runtime:'docker',imageId:'sha256:'+'a'.repeat(64),python:'python3',hermesSource:home,hermesHome:home,network:'none'}},home,script)
  84. try {
  85. await runtime.ready
  86. vi.spyOn(runtime,'resolve').mockResolvedValue({type:'resolved',model:{provider:'custom',api_mode:'chat_completions',model:'fixture'}})
  87. const ensure=vi.spyOn(runtime.provider,'ensure')
  88. const result=JSON.parse(await runtime.extractMemory('default','只提炼已给定的事实'))
  89. expect(result).toEqual({tools:[],prompt:'只提炼已给定的事实',history:[]})
  90. expect(ensure).not.toHaveBeenCalled()
  91. expect(runtime.workers.size).toBe(0)
  92. } finally { await runtime.pool.close();db.close();await rm(home,{recursive:true,force:true}) }
  93. })