imageManagement.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. // @vitest-environment node
  2. import {it,expect,vi} from 'vitest'
  3. import {DatabaseSync} from 'node:sqlite'
  4. import {mkdtemp,rm,writeFile} from 'node:fs/promises'
  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. import {ImageManagement} from '../../src/runner/computers/imageManagement'
  10. import {fileSHA256} from '../../src/runner/computers/images'
  11. it('persists image selection, rejects in-use switching and recovers interrupted jobs without replay',async()=>{
  12. const home=await mkdtemp(join(tmpdir(),'yaoyao-images-ui-')),db=new DatabaseSync(':memory:'),initial='sha256:'+'a'.repeat(64),next='sha256:'+'b'.repeat(64)
  13. const config:any={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:initial,python:'/fixture/python',hermesSource:'/fixture/source',hermesHome:'/fixture/home'}}
  14. const runtime=new ComputerRuntime(db,config,home);let manager:ImageManagement|undefined
  15. try{await runtime.ready;manager=new ImageManagement(runtime,db,home);await manager.ready
  16. const manifest={protocol:1 as const,imageId:next,architecture:'arm64' as const,driver:'0.20.0' as const,layer:'5' as const,createdAt:1}
  17. vi.spyOn(manager.images,'inspect').mockResolvedValue(manifest)
  18. vi.spyOn(runtime.provider,'verifyRuntime').mockResolvedValue();vi.spyOn(runtime.provider,'ensure').mockResolvedValue({id:'fixture',containerId:'fixture',workspace:'/fixture',running:true,isolation:'container'});vi.spyOn(runtime.provider,'health').mockResolvedValue({});vi.spyOn(runtime.provider,'capture').mockResolvedValue({data:'fixture',width:1,height:1,capturedAt:1});vi.spyOn(runtime.provider,'remove').mockResolvedValue();vi.spyOn(runtime.provider,'deleteWorkspace').mockResolvedValue()
  19. const release=runtime.pool.beginMaintenance();await expect(manager.start('owner',randomUUID(),'activate',{imageId:next},async()=>{})).rejects.toMatchObject({code:'computer_busy'});release()
  20. const id=randomUUID();await manager.start('owner',id,'activate',{imageId:next},async()=>{});await vi.waitFor(()=>expect(runtime.config.imageId).toBe(next));await manager.close()
  21. expect(JSON.parse(String(db.prepare('SELECT value FROM managed_image_selection').get()!.value))).toMatchObject({active:next,previous:initial,base:initial})
  22. config.computers.imageId=initial
  23. const resumed=new ImageManagement(runtime,db,home);await resumed.ready;expect(runtime.config.imageId).toBe(next);await resumed.close()
  24. }finally{await manager?.close();await runtime.controls.close();await runtime.pool.close();db.close();await rm(home,{recursive:true,force:true})}
  25. })
  26. it('uploads ordered bounded chunks, detects conflicting retries and isolates the uploader',async()=>{
  27. const home=await mkdtemp(join(tmpdir(),'yaoyao-images-upload-')),db=new DatabaseSync(':memory:'),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',hermesHome:'/fixture',hermesSource:'/fixture'}},home)
  28. const manager=new ImageManagement(runtime,db,home)
  29. try{await manager.ready;const id=randomUUID(),data=Buffer.from('image-fixture');const path=join(home,'hash');await writeFile(path,data)
  30. await manager.beginUpload('owner',id,data.length,{protocol:1,imageId:'sha256:'+'a'.repeat(64),architecture:'arm64',driver:'0.20.0',layer:'5',createdAt:1,archiveSha256:await fileSHA256(path)})
  31. const bytes=data.toString('base64');expect(await manager.appendUpload('owner',id,0,bytes)).toEqual({offset:data.length});expect(await manager.appendUpload('owner',id,0,bytes)).toEqual({offset:data.length})
  32. await expect(manager.appendUpload('other',id,0,bytes)).rejects.toMatchObject({code:'image_upload_missing'})
  33. await expect(manager.appendUpload('owner',id,0,Buffer.from('wrong').toString('base64'))).rejects.toThrow()
  34. await manager.cancelUpload('owner',id)
  35. }finally{await manager.close();await runtime.controls.close();await runtime.pool.close();db.close();await rm(home,{recursive:true,force:true})}
  36. })
  37. it('keeps the node in maintenance until uncertain validation cleanup is confirmed',async()=>{
  38. const home=await mkdtemp(join(tmpdir(),'yaoyao-images-cleanup-')),db=new DatabaseSync(':memory:'),initial='sha256:'+'a'.repeat(64)
  39. 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:initial,python:'/fixture',hermesHome:'/fixture',hermesSource:'/fixture'}},home),manager=new ImageManagement(runtime,db,home)
  40. let stopped=false
  41. try{await manager.ready;vi.spyOn(manager.images,'inspect').mockResolvedValue({protocol:1,imageId:'sha256:'+'b'.repeat(64),architecture:'arm64',driver:'0.20.0',layer:'5',createdAt:1});vi.spyOn(manager.images,'run').mockResolvedValue('');vi.spyOn(runtime.provider,'verifyRuntime').mockResolvedValue();vi.spyOn(runtime.provider,'ensure').mockResolvedValue({id:'fixture',containerId:'fixture',running:true,workspace:'/fixture',isolation:'container'});vi.spyOn(runtime.provider,'health').mockResolvedValue({});vi.spyOn(runtime.provider,'capture').mockResolvedValue({data:'fixture',width:1,height:1,capturedAt:1});vi.spyOn(runtime.provider,'remove').mockImplementation(async()=>{if(!stopped)throw new Error('stop uncertain')});vi.spyOn(runtime.provider,'deleteWorkspace').mockResolvedValue()
  42. await manager.start('owner',randomUUID(),'activate',{imageId:'sha256:'+'b'.repeat(64)},async()=>{})
  43. await vi.waitFor(async()=>expect((await manager.inventory('owner')).jobs[0]?.state).toBe('uncertain'))
  44. expect(()=>runtime.pool.beginMaintenance()).toThrow('请先停止');expect(runtime.config.imageId).toBe(initial)
  45. stopped=true;expect((await manager.inventory('owner')).busy).toBe(false)
  46. const release=runtime.pool.beginMaintenance();release()
  47. }finally{stopped=true;await manager.close();await runtime.controls.close();await runtime.pool.close();db.close();await rm(home,{recursive:true,force:true})}
  48. })