executionProxyRuntime.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. // @vitest-environment node
  2. import {afterEach,beforeEach,expect,it,vi} from 'vitest'
  3. import {DatabaseSync} from 'node:sqlite'
  4. import {mkdtempSync,rmSync} from 'node:fs'
  5. import {tmpdir} from 'node:os'
  6. import {join} from 'node:path'
  7. import {randomUUID} from 'node:crypto'
  8. import {ComputerRuntime,type ComputerGateway} from '../../src/runner/worker/gateway'
  9. import {ManagedProxy} from '../../src/runner/network/managedProxy'
  10. import type {VmProxySettings} from '../../src/shared/executionEnvironment'
  11. let home:string,db:DatabaseSync,runtime:ComputerRuntime
  12. const proxy=(revision:number):VmProxySettings=>({enabled:true,protocol:'http',host:'proxy.invalid',port:3128,username:'',revision})
  13. beforeEach(async()=>{
  14. home=mkdtempSync(join(tmpdir(),'yaoyao-proxy-runtime-'));db=new DatabaseSync(':memory:')
  15. runtime=new ComputerRuntime(db,{protocol:1,runnerId:randomUUID(),serverURL:'http://127.0.0.1',hermesURL:'http://127.0.0.1',token:'fixture',allowedProfiles:[],artifactRoots:[],computers:{runtime:'docker',imageId:'sha256:'+'a'.repeat(64),python:'python3',hermesSource:home,network:'none'}},home)
  16. await runtime.ready
  17. })
  18. afterEach(async()=>{await runtime.controls.close();await runtime.pool.close();db.close();vi.restoreAllMocks();rmSync(home,{recursive:true,force:true})})
  19. it('serializes updates, cleans up with the old network and rejects new leases while switching',async()=>{
  20. let unblock!:()=>void
  21. const blocked=new Promise<void>(resolve=>{unblock=resolve})
  22. const close=vi.fn(async()=>{expect(runtime.network).toBe('none');await blocked;runtime.gateways.clear()})
  23. runtime.gateways.set('fixture',new Set([{close} as unknown as ComputerGateway]))
  24. const verify=vi.spyOn(runtime.provider,'verifyNetwork').mockResolvedValue()
  25. const first=runtime.updateProxy(proxy(1)),second=runtime.updateProxy(proxy(1))
  26. await vi.waitFor(()=>expect(close).toHaveBeenCalledTimes(1))
  27. expect(()=>runtime.assertNetworkReady()).toThrow('尚未就绪')
  28. expect(verify).not.toHaveBeenCalled()
  29. unblock();await Promise.all([first,second])
  30. expect(verify).toHaveBeenCalledTimes(1)
  31. expect(runtime.network).toBe('managed-proxy')
  32. expect(()=>runtime.assertNetworkReady()).not.toThrow()
  33. await runtime.updateProxy(proxy(0))
  34. expect(runtime.proxySettings?.revision).toBe(1)
  35. })
  36. it('keeps a failed proxy preparation closed and allows retry of the same revision',async()=>{
  37. const verify=vi.spyOn(runtime.provider,'verifyNetwork').mockRejectedValueOnce(new Error('unavailable')).mockResolvedValue()
  38. await expect(runtime.updateProxy(proxy(1))).rejects.toThrow('unavailable')
  39. expect(runtime.network).toBe('managed-proxy')
  40. expect(()=>runtime.assertNetworkReady()).toThrow()
  41. await runtime.updateProxy(proxy(1))
  42. expect(verify).toHaveBeenCalledTimes(2)
  43. expect(()=>runtime.assertNetworkReady()).not.toThrow()
  44. })
  45. it('does not share a failed network channel with the next task',async()=>{
  46. vi.spyOn(runtime.provider,'verifyNetwork').mockResolvedValue()
  47. await runtime.updateProxy(proxy(1))
  48. let stopped!:()=>void
  49. const old={close:vi.fn(async()=>{})},next={close:vi.fn(async()=>{})}
  50. const start=vi.spyOn(ManagedProxy,'start').mockImplementationOnce(async(_provider,_spec,_proxy,_authorize,onClose)=>{stopped=onClose!;return old as unknown as ManagedProxy}).mockResolvedValueOnce(next as unknown as ManagedProxy)
  51. const spec={id:randomUUID(),ownerKey:'fixture',imageId:'sha256:'+'a'.repeat(64),cwd:'/workspace',network:'managed-proxy' as const}
  52. const first=await runtime.acquireProxy(spec,()=>{})
  53. stopped()
  54. const second=await runtime.acquireProxy(spec,()=>{})
  55. expect(start).toHaveBeenCalledTimes(2)
  56. await first.release()
  57. expect(next.close).not.toHaveBeenCalled()
  58. await second.release()
  59. expect(next.close).toHaveBeenCalledTimes(1)
  60. })
  61. it('uses the same proxy for human takeover and ends old control leases when the proxy changes',async()=>{
  62. vi.spyOn(runtime.provider,'verifyNetwork').mockResolvedValue()
  63. vi.spyOn(runtime,'resolveWorkspace').mockResolvedValue({type:'resolved',cwd:'/workspace',configuredCwd:'/workspace'})
  64. const ensure=vi.spyOn(runtime.provider,'ensure').mockResolvedValue({id:'fixture',containerId:'fixture',running:true,workspace:home,isolation:'container'})
  65. vi.spyOn(runtime.provider,'stop').mockResolvedValue();vi.spyOn(runtime.provider,'remove').mockResolvedValue()
  66. const close=vi.fn(async()=>{})
  67. vi.spyOn(ManagedProxy,'start').mockResolvedValue({close} as unknown as ManagedProxy)
  68. await runtime.updateProxy(proxy(1))
  69. const meta={environmentId:randomUUID(),agentId:randomUUID(),ownerKey:'fixture'},controlId=randomUUID()
  70. await runtime.controls.take(meta,'default',controlId,async()=>{})
  71. await vi.waitFor(async()=>expect((await runtime.controls.status(meta)).mode).toBe('human'))
  72. expect(ensure.mock.calls[0]?.[0].network).toBe('managed-proxy')
  73. await runtime.updateProxy(proxy(2))
  74. expect(close).toHaveBeenCalled()
  75. expect((await runtime.controls.status(meta)).controlId).toBeUndefined()
  76. expect(runtime.pool.hasHolders(meta.ownerKey,meta.environmentId)).toBe(false)
  77. await runtime.controls.take(meta,'default',randomUUID(),async()=>{})
  78. await vi.waitFor(async()=>expect((await runtime.controls.status(meta)).mode).toBe('human'))
  79. })