computerMaintenance.test.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. // @vitest-environment node
  2. import {it,expect,vi} from 'vitest'
  3. import {mkdtemp,mkdir,writeFile,readFile,rm,symlink,lstat} from 'node:fs/promises'
  4. import {tmpdir} from 'node:os'
  5. import {join,basename} from 'node:path'
  6. import {randomUUID} from 'node:crypto'
  7. import {ComputerMaintenance} from '../../src/runner/computers/maintenance'
  8. import {recoverWorkspace} from '../../src/runner/computers/workspaceRecovery'
  9. it('backs up only stopped owned data and validates restore before replacing it',async()=>{
  10. const home=await mkdtemp(join(tmpdir(),'yaoyao-maintenance-')),id=randomUUID(),workspace=join(home,'computer-workspaces',id),spec={id,ownerKey:'fixture',imageId:'sha256:'+'a'.repeat(64)}
  11. let status='active'
  12. const pool={status:()=>[{environmentId:id,status}]},provider={remove:vi.fn().mockResolvedValue(undefined)}
  13. const maintenance=new ComputerMaintenance(pool as any,provider as any,home),snapshot=join(home,'snapshot')
  14. try{
  15. await mkdir(workspace,{recursive:true});await writeFile(join(workspace,'document.txt'),'before');await symlink('/guest/path',join(workspace,'guest-link'))
  16. await expect(maintenance.backup(spec,snapshot)).rejects.toThrow('停止状态')
  17. status='free';await maintenance.backup(spec,snapshot)
  18. await writeFile(join(workspace,'document.txt'),'after')
  19. const result=await maintenance.restore(spec,snapshot)
  20. expect(await readFile(join(workspace,'document.txt'),'utf8')).toBe('before')
  21. expect(await readFile(join(result.previousWorkspace,'document.txt'),'utf8')).toBe('after')
  22. expect((await lstat(join(workspace,'guest-link'))).isSymbolicLink()).toBe(true)
  23. await writeFile(join(snapshot,'workspace/document.txt'),'damaged')
  24. await expect(maintenance.restore(spec,snapshot)).rejects.toThrow('校验失败')
  25. expect(await readFile(join(workspace,'document.txt'),'utf8')).toBe('before')
  26. await expect(maintenance.backup(spec,snapshot)).rejects.toMatchObject({code:'EEXIST'})
  27. }finally{await rm(home,{recursive:true,force:true})}
  28. })
  29. it('recovers an interrupted content restore without replacing the bind-mount root',async()=>{
  30. const base=await mkdtemp(join(tmpdir(),'yaoyao-restore-')),id=randomUUID(),workspace=join(base,id),previous=join(base,`.before-restore-${id}-${randomUUID()}`),staging=join(base,`.restore-${randomUUID()}`)
  31. try{
  32. await mkdir(workspace);await writeFile(join(workspace,'file'),'original');await mkdir(staging);await writeFile(join(staging,'file'),'replacement')
  33. await writeFile(join(base,`.restore-${id}.json`),JSON.stringify({protocol:1,mode:'copy',previous:basename(previous),staging:basename(staging)}))
  34. await mkdir(previous);await writeFile(join(previous,'file'),'original');await writeFile(join(workspace,'file'),'partial replacement');const inode=(await lstat(workspace)).ino;await recoverWorkspace(base,id)
  35. expect((await lstat(workspace)).ino).toBe(inode)
  36. expect(await readFile(join(workspace,'file'),'utf8')).toBe('original')
  37. await expect(lstat(staging)).rejects.toMatchObject({code:'ENOENT'})
  38. }finally{await rm(base,{recursive:true,force:true})}
  39. })
  40. it('backs up and restores private application data together with the workspace and checks tampering first',async()=>{
  41. const home=await mkdtemp(join(tmpdir(),'yaoyao-private-backup-')),id=randomUUID(),workspace=join(home,'computer-workspaces',id),data=join(home,'computer-userdata',id)
  42. const spec={id,ownerKey:'fixture',imageId:'sha256:'+'a'.repeat(64)},snapshot=join(home,'snapshot')
  43. const maintenance=new ComputerMaintenance({status:()=>[{environmentId:id,status:'free'}]} as any,{remove:async()=>{}} as any,home)
  44. try{
  45. await mkdir(workspace,{recursive:true});await mkdir(join(data,'home','.local','share','keyrings'),{recursive:true})
  46. await mkdir(join(data,'home','workspace'));const nestedMountInode=(await lstat(join(data,'home','workspace'))).ino
  47. await writeFile(join(workspace,'file'),'before');await writeFile(join(data,'home','.local','share','keyrings','login'),'encrypted-fixture')
  48. await maintenance.backup(spec,snapshot)
  49. await writeFile(join(workspace,'file'),'after');await writeFile(join(data,'home','.local','share','keyrings','login'),'new-fixture')
  50. const result=await maintenance.restore(spec,snapshot)
  51. expect(result.userDataRestored).toBe(true)
  52. expect((await lstat(join(data,'home','workspace'))).ino).toBe(nestedMountInode)
  53. expect(await readFile(join(data,'home','.local','share','keyrings','login'),'utf8')).toBe('encrypted-fixture')
  54. expect(await readFile(join(result.previousUserData!,'home','.local','share','keyrings','login'),'utf8')).toBe('new-fixture')
  55. await writeFile(join(snapshot,'userdata','home','.local','share','keyrings','login'),'tampered')
  56. await writeFile(join(workspace,'file'),'must-remain')
  57. await expect(maintenance.restore(spec,snapshot)).rejects.toThrow('校验失败')
  58. expect(await readFile(join(workspace,'file'),'utf8')).toBe('must-remain')
  59. }finally{await rm(home,{recursive:true,force:true})}
  60. })
  61. it('rolls both workspace and credentials back after an interrupted restore',async()=>{
  62. const home=await mkdtemp(join(tmpdir(),'yaoyao-private-restore-')),id=randomUUID(),base=join(home,'computer-workspaces'),dataBase=join(home,'computer-userdata')
  63. const previousName=`.before-restore-${id}-${randomUUID()}`,stagingName=`.restore-${randomUUID()}`
  64. try{
  65. for(const parent of [base,dataBase]){
  66. for(const name of [id,previousName,stagingName])await mkdir(join(parent,name),{recursive:true})
  67. await writeFile(join(parent,id,'data'),'partially-restored')
  68. await writeFile(join(parent,previousName,'data'),'original')
  69. }
  70. await writeFile(join(base,`.restore-${id}.json`),JSON.stringify({protocol:1,mode:'copy',previous:previousName,staging:stagingName,userData:{previous:previousName,staging:stagingName}}))
  71. await recoverWorkspace(base,id)
  72. for(const parent of [base,dataBase])expect(await readFile(join(parent,id,'data'),'utf8')).toBe('original')
  73. await expect(lstat(join(base,`.restore-${id}.json`))).rejects.toMatchObject({code:'ENOENT'})
  74. }finally{await rm(home,{recursive:true,force:true})}
  75. })