localVmImages.test.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import {it,expect,vi,afterEach} from 'vitest'
  2. import {mount,flushPromises} from '@vue/test-utils'
  3. import Settings from '../../src/client/components/app/LocalVmSettingsPanel.vue'
  4. import Panel from '../../src/client/components/workspace/LocalVmChatPanel.vue'
  5. import {apiRequest} from '../../src/client/api/client'
  6. vi.mock('../../src/client/api/client',()=>({apiRequest:vi.fn()}))
  7. const images=[{key:'standard',name:'标准桌面',description:'XFCE',ready:true,imageId:'sha256:a'},{key:'cursor',name:'Cursor Universal',description:'Debian',ready:true,imageId:'sha256:b'}]
  8. const agent={id:'image-fixture',name:'镜像测试',computer:'vm',execution:'computer',nodeId:'local',profile:'default'} as any
  9. const vm={configured:true,runtime:'docker',daemonUp:true,image:true,mode:'per-bot',maxInstances:2,busy:false,images}
  10. it('replaces the legacy execution-mode select with parallel toggles and locks them while in use',async()=>{
  11. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  12. let inUse=false
  13. vi.mocked(apiRequest).mockImplementation(async path=>{
  14. if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
  15. if(path.endsWith('/cloud-computer'))return {configured:false} as any
  16. return {...vm,enabled:true,container:'missing',ready:false,inUse} as any
  17. })
  18. const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  19. try{
  20. await flushPromises()
  21. expect(wrapper.find('[aria-label="本地虚拟机执行方式"]').exists()).toBe(false)
  22. expect((wrapper.get('[aria-label="本地虚拟机"]').element as HTMLInputElement).checked).toBe(true)
  23. await wrapper.get('[aria-label="本机控制"]').setValue(true);await flushPromises()
  24. expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:true,cloud:false,desktop:true,browser:false}}})
  25. inUse=true;await wrapper.setProps({active:false});await wrapper.setProps({active:true});await flushPromises()
  26. expect((wrapper.get('[aria-label="本机控制"]').element as HTMLInputElement).disabled).toBe(true)
  27. }finally{wrapper.unmount()}
  28. })
  29. afterEach(()=>{vi.clearAllMocks();vi.restoreAllMocks()})
  30. it('saves never-stop and restores the selected value after refresh',async()=>{
  31. let idleStopMinutes=5
  32. vi.mocked(apiRequest).mockImplementation(async(path,options:any)=>{
  33. if(path.endsWith('/idle-policy'))idleStopMinutes=options.body.idleStopMinutes
  34. return path==='/api/app/computers'?{computers:[]}:{...vm,idleStopMinutes} as any
  35. })
  36. const wrapper=mount(Settings,{global:{stubs:{AppIcon:true,RunnerSettingsPanel:true}}})
  37. try{
  38. await flushPromises();await wrapper.get('[aria-label="虚拟机空闲停止时间"]').setValue('0');await flushPromises()
  39. expect(apiRequest).toHaveBeenCalledWith('/api/app/admin/local-vm/idle-policy',expect.objectContaining({body:expect.objectContaining({idleStopMinutes:0})}))
  40. expect((wrapper.get('select[aria-label="虚拟机空闲停止时间"]').element as HTMLSelectElement).value).toBe('0')
  41. expect(wrapper.text()).toContain('空闲时保持运行')
  42. }finally{wrapper.unmount()}
  43. })
  44. it('shows parallel toggles without a combined environment tab',async()=>{
  45. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  46. vi.mocked(apiRequest).mockImplementation(async path=>{
  47. if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
  48. if(path.endsWith('/cloud-computer'))return {configured:false} as any
  49. return {...vm,enabled:true,container:'missing',ready:false,inUse:false} as any
  50. })
  51. const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  52. try{
  53. await flushPromises()
  54. expect(wrapper.find('[role="tab"][aria-label="本机 + 虚拟机"]').exists()).toBe(false)
  55. await wrapper.get('[aria-label="云端 · Grok Bot"]').setValue(true);await flushPromises()
  56. expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:true,cloud:true,desktop:false,browser:false}}})
  57. }finally{wrapper.unmount()}
  58. })
  59. it('checks desktop alongside cloud and never shows the legacy host option',async()=>{
  60. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  61. vi.mocked(apiRequest).mockImplementation(async path=>{
  62. if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
  63. if(path.endsWith('/cloud-computer'))return {configured:true,running:false} as any
  64. return {...vm,enabled:false,container:'missing',ready:false,inUse:false} as any
  65. })
  66. const cloudAgent={...agent,computer:'cloud',execution:'profile'}
  67. const wrapper=mount(Panel,{props:{agents:[cloudAgent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  68. try{
  69. await flushPromises()
  70. expect((wrapper.get('[aria-label="云端 · Grok Bot"]').element as HTMLInputElement).checked).toBe(true)
  71. await wrapper.get('[aria-label="本机控制"]').setValue(true);await flushPromises()
  72. expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{envs:{vm:false,cloud:true,desktop:true,browser:false}}})
  73. for(const computer of ['auto','off','local','browser']){
  74. await wrapper.setProps({agents:[{...cloudAgent,computer}]});await flushPromises()
  75. expect(wrapper.find('.host-environment-option').exists()).toBe(false)
  76. }
  77. }finally{wrapper.unmount()}
  78. })
  79. it('keeps Compose lifecycle controls hidden while enabling host tools for an already assigned desktop',async()=>{
  80. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  81. vi.mocked(apiRequest).mockImplementation(async path=>{
  82. if(path==='/api/app/computers')return {computers:[]} as any
  83. if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
  84. if(path.endsWith('/cloud-computer'))return {configured:false} as any
  85. return {...vm,fixedCapacity:true,desktopId:'desktop',desktops:[{id:'desktop',name:'已有桌面',ready:true}],enabled:true,container:'running',ready:false,inUse:false} as any
  86. })
  87. const settings=mount(Settings,{global:{stubs:{AppIcon:true,RunnerSettingsPanel:true}}})
  88. const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  89. try{
  90. await flushPromises();expect(settings.find('[aria-label="虚拟机空闲停止时间"]').exists()).toBe(false)
  91. expect(wrapper.findAll('button').some(b=>b.text()==='停止虚拟机')).toBe(false)
  92. await wrapper.get('input[type="checkbox"]').setValue(true);await flushPromises()
  93. expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/computer-selection',{method:'PUT',body:{computer:'vm',allowHostEnvironment:true}})
  94. }finally{settings.unmount();wrapper.unmount()}
  95. })
  96. it('prepares the selected image in Settings and explains the shared image constraint',async()=>{
  97. vi.mocked(apiRequest).mockImplementation(async path=>path==='/api/app/computers'?{computers:[]}:vm as any)
  98. const wrapper=mount(Settings,{global:{stubs:{AppIcon:true,RunnerSettingsPanel:true}}})
  99. try{
  100. await flushPromises()
  101. const select=wrapper.findAll('select').find(select=>select.text().includes('Cursor Universal'))!
  102. await select.setValue('cursor')
  103. await wrapper.findAll('button').find(b=>b.text()==='重新检查所选镜像')!.trigger('click')
  104. await flushPromises()
  105. expect(apiRequest).toHaveBeenCalledWith('/api/app/admin/local-vm/prepare',expect.objectContaining({body:expect.objectContaining({imageKey:'cursor'})}))
  106. expect(wrapper.text()).toContain('同一共享桌面的所有机器人使用一个镜像')
  107. }finally{wrapper.unmount()}
  108. })
  109. it('selects a prepared desktop image and locks the selection for an existing shared instance',async()=>{
  110. vi.spyOn(document,'hidden','get').mockReturnValue(false)
  111. let detail={...vm,enabled:true,container:'missing',ready:false,inUse:false,imageKey:'standard'}
  112. vi.mocked(apiRequest).mockImplementation(async path=>{
  113. if(path.endsWith('/desktop-environment'))return {selected:null,local:{supported:false},browser:{available:false}} as any
  114. if(path.endsWith('/cloud-computer'))return {configured:false} as any
  115. return detail as any
  116. })
  117. const wrapper=mount(Panel,{props:{agents:[agent],isAdmin:true,active:true},global:{stubs:{AppIcon:true,GrokAuthPanel:true}}})
  118. try{
  119. await flushPromises()
  120. const select=wrapper.findAll('select').find(s=>s.text().includes('Cursor Universal'))!
  121. await select.setValue('cursor');await flushPromises()
  122. expect(apiRequest).toHaveBeenCalledWith('/api/app/agents/image-fixture/local-vm/image',{method:'PUT',body:{imageKey:'cursor'}})
  123. detail={...detail,mode:'shared',container:'stopped'}
  124. await wrapper.setProps({active:false});await wrapper.setProps({active:true});await flushPromises()
  125. expect(select.attributes('disabled')).toBeDefined()
  126. expect(wrapper.text()).toContain('同一共享桌面只能使用一个镜像')
  127. }finally{wrapper.unmount()}
  128. })