computer.live.test.ts 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. // @vitest-environment node
  2. import {expect,it} from 'vitest'
  3. import {mkdtemp,rm,readFile,writeFile,mkdir} from 'node:fs/promises'
  4. import {tmpdir} from 'node:os'
  5. import {join} from 'node:path'
  6. import {DatabaseSync} from 'node:sqlite'
  7. import {ComputerMaintenance} from '../../src/runner/computers/maintenance'
  8. import {ComputerPool} from '../../src/runner/computers/pool'
  9. import {randomUUID} from 'node:crypto'
  10. import {ContainerComputerProvider,CUA_DRIVER,CUA_SOCKET,type ComputerSpecification} from '../../src/runner/computers/container'
  11. // Only creates a UUID-owned private fixture; never adopts user containers.
  12. it.skipIf(!process.env.YAOYAO_COMPUTER_IMAGE)('executes in a real private computer and preserves its workspace across recreation',async()=>{
  13. const home=await mkdtemp(join(tmpdir(),'yaoyao-computer-live-'))
  14. const provider=new ContainerComputerProvider('docker',randomUUID(),home)
  15. const spec:ComputerSpecification={id:randomUUID(),ownerKey:randomUUID(),imageId:process.env.YAOYAO_COMPUTER_IMAGE!}
  16. const db=new DatabaseSync(':memory:'),pool=new ComputerPool(db,provider)
  17. try {
  18. await pool.recover()
  19. const lease=await pool.acquire(spec,'initial-task',()=>{})
  20. const initial=(await provider.inspect(spec))!
  21. expect(initial.running).toBe(true)
  22. expect((await provider.execute(spec,['ls','/sys/class/net'],{authorize:()=>{}})).stdout.trim()).toBe('lo')
  23. const result=await provider.execute(spec,['/bin/sh','-c','id -u; printf isolated-fixture > persistence-proof.txt; pwd'],{authorize:()=>{}})
  24. expect(result.stdout).toContain('1000\n');expect(result.stdout).toContain('/home/cua/workspace')
  25. expect(await readFile(join(initial.workspace,'persistence-proof.txt'),'utf8')).toBe('isolated-fixture')
  26. let healthError=''
  27. try{await expect.poll(async()=>{
  28. try{return await provider.health(spec,()=>{})}catch(error){healthError=`${String(error)} ${String((error as any).stderr??'')}`;return null}
  29. },{timeout:60000,interval:1000}).toMatchObject({schema_version:'1'})}catch{
  30. const diagnostics=await provider.run('docker',['logs','--tail','70',initial.containerId]).catch(()=>({stdout:'',stderr:''}))
  31. const vnc=await provider.run('docker',['exec','--user','0',initial.containerId,'/bin/sh','-c','tail -40 /var/log/supervisor/vncserver* /home/cua/.vnc/*.log 2>/dev/null']).catch(error=>({stdout:String(error.stdout??''),stderr:String(error.stderr??'')}))
  32. throw new Error(`Computer health: ${healthError}\n${diagnostics.stdout}\n${diagnostics.stderr}\n${vnc.stdout}\n${vnc.stderr}`)
  33. }
  34. await provider.execute(spec,[CUA_DRIVER,'call','get_desktop_state','{}','--socket',CUA_SOCKET,'--screenshot-out-file','/tmp/yaoyao-computer-proof.png'],{authorize:()=>{}})
  35. const screenshot=Buffer.from((await provider.execute(spec,['base64','-w0','/tmp/yaoyao-computer-proof.png'],{authorize:()=>{}})).stdout.trim(),'base64')
  36. expect(screenshot.subarray(1,4).toString()).toBe('PNG');expect(screenshot.byteLength).toBeGreaterThan(1024)
  37. await mkdir('test-results/computer',{recursive:true});await writeFile('test-results/computer/desktop.png',screenshot)
  38. await pool.release(lease)
  39. const nextLease=await pool.acquire(spec,'next-task',()=>{})
  40. const recreated=(await provider.inspect(spec))!
  41. expect(recreated.containerId).not.toBe(initial.containerId)
  42. expect((await provider.execute(spec,['cat','persistence-proof.txt'],{authorize:()=>{}})).stdout).toBe('isolated-fixture')
  43. const pending=pool.use(nextLease,context=>provider.execute(spec,['/bin/sh','-c','sleep 30; printf should-not-run > cancelled-proof.txt'],context))
  44. const settled=pending.catch(error=>error)
  45. await new Promise(resolve=>setTimeout(resolve,500));await pool.release(nextLease)
  46. expect(await settled).toBeInstanceOf(Error)
  47. expect(await provider.inspect(spec)).toBeUndefined()
  48. expect(()=>pool.use(lease,async()=>{})).toThrow('电脑控制权已改变')
  49. await expect(readFile(join(initial.workspace,'cancelled-proof.txt'))).rejects.toMatchObject({code:'ENOENT'})
  50. const maintenance=new ComputerMaintenance(pool,provider,home),snapshot=join(home,'snapshot')
  51. await maintenance.backup(spec,snapshot)
  52. await writeFile(join(initial.workspace,'persistence-proof.txt'),'changed-after-backup')
  53. const restored=await maintenance.restore(spec,snapshot)
  54. expect(await readFile(join(restored.previousWorkspace,'persistence-proof.txt'),'utf8')).toBe('changed-after-backup')
  55. const restoredLease=await pool.acquire(spec,'restored-task',()=>{})
  56. expect((await provider.execute(spec,['cat','persistence-proof.txt'],{authorize:()=>{}})).stdout).toBe('isolated-fixture')
  57. await pool.release(restoredLease)
  58. }catch(error){console.error('电脑验收错误',String((error as any).stderr??''),String((error as any).stdout??''));throw error}finally{await pool.close();await provider.remove(spec);db.close();await rm(home,{recursive:true,force:true})}
  59. },100000)