localVmStartup.test.ts 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. // @vitest-environment node
  2. import {expect,it,vi} from 'vitest'
  3. import {mkdtempSync,rmSync} from 'node:fs'
  4. import {tmpdir} from 'node:os'
  5. import {join} from 'node:path'
  6. import {randomUUID} from 'node:crypto'
  7. import {WorkspaceStore} from '../../src/server/workspaceStore'
  8. import {LocalVmService} from '../../src/server/localVm'
  9. import {saveHostTools} from '../../src/server/hostToolSettings'
  10. const runners=vi.hoisted(()=>[] as any[])
  11. vi.mock('../../src/runner/agent',()=>({RunnerAgent:class {
  12. onConnected=()=>{}
  13. constructor(readonly config:any){runners.push(this)}
  14. async run(signal:AbortSignal){this.onConnected();await new Promise<void>(done=>signal.addEventListener('abort',()=>done(),{once:true}))}
  15. }}))
  16. function fixture(){
  17. const home=mkdtempSync(join(tmpdir(),'yaoyao-vm-startup-')),store=new WorkspaceStore(home)
  18. saveHostTools(home,{vm:true})
  19. const record={id:randomUUID(),enabled:true,sourceNodeId:'local',sourceOwner:'_system'}
  20. const auth={isUserActive:(owner:string)=>owner==='alice'} as any
  21. const nodes={open:()=>({runnerId:record.id}),requireSource:vi.fn((_owner:string,agent:any)=>{if(agent.profile==='denied')throw new Error('unauthorized')})} as any
  22. const hub={records:()=>[record],computerRunner:()=>record,computer:vi.fn(async(_owner:string,_agent:any,_op:string,_data:any,authorize:()=>void)=>{authorize();return {ok:true}})} as any
  23. const shared={reconcileLocalVmGroups:vi.fn(),attachLocalVm:vi.fn()} as any
  24. const service=new LocalVmService(store,auth,nodes,hub,shared,{home} as any)
  25. store.put('_system','local-vm-managed','local',{sealed:'fixture'})
  26. const add=(owner='alice',changes:Record<string,unknown>={})=>{
  27. const agent=store.createAgent(owner,{name:`启动测试 ${randomUUID()}`,profile:'default'})
  28. Object.assign(agent,changes);store.put(owner,'agent',agent.id,agent);return agent
  29. }
  30. return {home,store,service,hub,shared,record,add,
  31. async settled(){await (service as any).managed?.warmup},
  32. async close(){await service.close();store.close();rmSync(home,{recursive:true,force:true})},
  33. }
  34. }
  35. it('starts authorized configured desktops once per service start and deduplicates shared members',async()=>{
  36. const f=fixture()
  37. try{
  38. const group=randomUUID(),first=f.add('alice',{computerEnvironmentId:group})
  39. f.add('alice',{computerEnvironmentId:group});const own=f.add()
  40. f.add('alice',{archived:true});f.add('alice',{temporaryGoalId:randomUUID()})
  41. f.add('alice',{remoteAgentId:randomUUID()});f.add('alice',{nodeId:'remote'})
  42. f.add('alice',{profile:'denied'});f.add('bob')
  43. await f.service.start('http://127.0.0.1:15300');await f.settled()
  44. expect(f.hub.computer.mock.calls.map((c:any[])=>(c[1].computerEnvironmentId??c[1].id)).sort()).toEqual([group,own.id].sort())
  45. expect(f.hub.computer).toHaveBeenCalledWith('alice',expect.objectContaining({computerEnvironmentId:first.computerEnvironmentId}),'autostart',{},expect.any(Function))
  46. // Reconnection, repeated start(), or polling settings must not undo a stop.
  47. runners.at(-1).onConnected()
  48. await f.service.start('http://127.0.0.1:15300');await f.settled()
  49. expect(f.hub.computer).toHaveBeenCalledTimes(2)
  50. await f.service.close();await f.service.start('http://127.0.0.1:15300');await f.settled()
  51. expect(f.hub.computer).toHaveBeenCalledTimes(4)
  52. }finally{await f.close()}
  53. })
  54. it('respects the global VM switch and rechecks authorization before each admitted start',async()=>{
  55. const f=fixture()
  56. try{
  57. f.add();saveHostTools(f.home,{vm:false})
  58. await f.service.start('http://127.0.0.1:15300');await f.settled()
  59. expect(f.hub.computer).not.toHaveBeenCalled()
  60. await f.service.close();saveHostTools(f.home,{vm:true})
  61. f.hub.computer.mockImplementation(async(_owner:string,_agent:any,_op:string,_data:any,authorize:()=>void)=>{
  62. saveHostTools(f.home,{vm:false});expect(authorize).toThrow('虚拟机启动授权已失效')
  63. })
  64. await f.service.start('http://127.0.0.1:15300');await f.settled()
  65. expect(f.hub.computer).toHaveBeenCalledOnce()
  66. }finally{await f.close()}
  67. })
  68. it('keeps Web startup available if one desktop fails, and attempts the remaining desktops',async()=>{
  69. const f=fixture()
  70. try{
  71. f.add();f.add()
  72. f.hub.computer.mockRejectedValueOnce(new Error('Docker unavailable'))
  73. await expect(f.service.start('http://127.0.0.1:15300')).resolves.toBeUndefined();await f.settled()
  74. expect(f.hub.computer).toHaveBeenCalledTimes(2)
  75. expect((f.service as any).problem).toContain('未能自动启动')
  76. }finally{await f.close()}
  77. })