computerControl.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445
  1. // @vitest-environment node
  2. import {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} from '../../src/runner/worker/gateway'
  9. it('separates view from control and fences stale, duplicate and out-of-frame input',async()=>{
  10. const home=mkdtempSync(join(tmpdir(),'yaoyao-control-')),db=new DatabaseSync(':memory:')
  11. 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:'/fixture/python',hermesSource:'/fixture/source',hermesHome:'/fixture/home'}},home)
  12. let permitted=true
  13. try{
  14. await runtime.ready
  15. vi.spyOn(runtime,'resolve').mockResolvedValue({type:'resolved',cwd:'/projects/fixture',configuredCwd:'/projects/fixture',model:{}})
  16. vi.spyOn(runtime.provider,'ensure').mockResolvedValue({id:'fixture',containerId:'fixture',running:true,workspace:'/fixture',isolation:'container'})
  17. vi.spyOn(runtime.provider,'stop').mockResolvedValue();vi.spyOn(runtime.provider,'remove').mockResolvedValue()
  18. vi.spyOn(runtime.provider,'capture').mockImplementation(async()=>({data:'image-fixture',width:100,height:80,capturedAt:Date.now()}))
  19. const execute=vi.spyOn(runtime.provider,'execute').mockResolvedValue({stdout:'{"ok":true}',stderr:''})
  20. const meta={environmentId:randomUUID(),agentId:randomUUID(),ownerKey:'owner'},id=randomUUID()
  21. await expect(runtime.controls.input(meta,id,randomUUID(),1,randomUUID(),{kind:'key',key:'Return'})).rejects.toMatchObject({code:'computer_control_required'})
  22. await runtime.controls.take(meta,'default',id,async()=>{if(!permitted)throw new Error('revoked')})
  23. await vi.waitFor(async()=>expect((await runtime.controls.status(meta)).mode).toBe('human'))
  24. const frame=await runtime.controls.frame(meta,()=>{})
  25. expect(execute).not.toHaveBeenCalled()
  26. await expect(runtime.controls.input(meta,id,randomUUID(),frame.generation-1,frame.id,{kind:'key',key:'Return'})).rejects.toMatchObject({code:'computer_control_stale'})
  27. await expect(runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,{kind:'click',x:101,y:1})).rejects.toMatchObject({code:'computer_point_invalid'})
  28. const requestId=randomUUID(),action={kind:'click',x:20,y:30}
  29. const first=await runtime.controls.input(meta,id,requestId,frame.generation,frame.id,action)
  30. expect(await runtime.controls.input(meta,id,requestId,frame.generation,frame.id,action)).toEqual(first)
  31. expect(execute).toHaveBeenCalledTimes(1)
  32. await expect(runtime.controls.input(meta,id,requestId,frame.generation,frame.id,{kind:'click',x:30,y:30})).rejects.toMatchObject({code:'idempotency_conflict'})
  33. await expect(runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,{kind:'drag',fromX:2,fromY:3,toX:101,toY:4})).rejects.toMatchObject({code:'computer_point_invalid'})
  34. await runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,{kind:'drag',fromX:2,fromY:3,toX:20,toY:30})
  35. expect(execute.mock.calls.at(-1)?.[1]).toContain('xdotool')
  36. await runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,{kind:'key',key:'a',modifiers:['ctrl']})
  37. expect(execute.mock.calls.at(-1)?.[1]).toContain('hotkey')
  38. expect(JSON.parse(execute.mock.calls.at(-1)![1][3]!)).toMatchObject({keys:['ctrl','a']})
  39. permitted=false
  40. await expect(runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,action)).rejects.toThrow('revoked')
  41. await vi.waitFor(async()=>expect((await runtime.controls.status(meta)).mode).toBe('off'),{timeout:6500,interval:100})
  42. expect(runtime.provider.stop).toHaveBeenCalled()
  43. await expect(runtime.controls.input(meta,id,randomUUID(),frame.generation,frame.id,action)).rejects.toMatchObject({code:'computer_control_required'})
  44. }finally{await runtime.controls.close();await runtime.pool.close();db.close();rmSync(home,{recursive:true,force:true})}
  45. },10000)