team-tools.spec.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { test, expect } from '@playwright/test'
  2. test('file memories and an independent project are editable on desktop and mobile', async ({ page }, testInfo) => {
  3. await page.goto('/conversations')
  4. await page.getByRole('textbox', { name: '账号', exact: true }).fill('fixture')
  5. await page.getByRole('textbox', { name: '密码', exact: true }).fill('fixture-pass')
  6. await page.getByRole('button', { name: '登录', exact: true }).click()
  7. for (const name of ['记忆甲', '记忆乙']) {
  8. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  9. await page.getByRole('menuitem', { name: '新建 Bot', exact: true }).click()
  10. const editor = page.getByRole('dialog', { name: '创建机器人' })
  11. await editor.getByRole('textbox', { name: '名称', exact: true }).fill(name)
  12. await editor.getByRole('button', { name: '保存', exact: true }).click()
  13. await expect(page.getByRole('heading', { name, exact: true })).toBeVisible()
  14. }
  15. await page.getByRole('button', { name: '记忆与协作', exact: true }).click()
  16. const panel = page.getByRole('dialog', { name: 'Bot 记忆与协作' })
  17. await panel.getByRole('button', { name: '项目', exact: true }).click()
  18. await panel.getByRole('button', { name: '新建项目', exact: true }).click()
  19. await panel.getByRole('textbox', { name: '名称', exact: true }).fill('文件记忆项目')
  20. await panel.getByRole('textbox', { name: '说明', exact: true }).fill('多位 Bot 共同维护项目事实')
  21. await panel.getByRole('group', { name: '项目成员', exact: true }).getByRole('checkbox', { name: '记忆甲', exact: true }).check()
  22. await panel.getByRole('group', { name: '项目成员', exact: true }).getByRole('checkbox', { name: '记忆乙', exact: true }).check()
  23. await panel.getByRole('button', { name: '保存项目', exact: true }).click()
  24. await expect(panel.getByRole('heading', { name: '文件记忆项目', exact: true })).toBeVisible()
  25. await panel.getByRole('button', { name: '项目记忆', exact: true }).click()
  26. await panel.getByRole('button', { name: '添加记忆', exact: true }).click()
  27. await panel.getByRole('textbox', { name: '事实', exact: true }).fill('接口变更必须兼容旧客户端')
  28. await panel.getByRole('button', { name: '保存', exact: true }).click()
  29. await expect(panel.locator('article').filter({ hasText: '接口变更必须兼容旧客户端' })).toBeVisible()
  30. const article = panel.locator('article').filter({ hasText: '接口变更必须兼容旧客户端' }).first()
  31. await article.getByRole('button', { name: '编辑', exact: true }).click()
  32. await panel.getByRole('textbox', { name: '事实', exact: true }).fill('接口变更需兼容旧客户端并注明版本')
  33. await panel.getByRole('button', { name: '保存', exact: true }).click()
  34. const edited = panel.locator('article').filter({ hasText: '接口变更需兼容旧客户端并注明版本' }).first()
  35. await expect(edited).toContainText('版本 2')
  36. await edited.getByRole('button', { name: '修订记录', exact: true }).click()
  37. await expect(panel.getByRole('heading', { name: '修订记录', exact: true })).toBeVisible()
  38. await page.screenshot({ path: testInfo.outputPath('bot-project-memory-desktop.png') })
  39. await page.setViewportSize({ width: 390, height: 844 })
  40. await page.emulateMedia({ colorScheme: 'dark', reducedMotion: 'reduce' })
  41. await expect(panel).toBeInViewport()
  42. expect(await panel.evaluate(el => el.scrollWidth <= el.clientWidth + 1)).toBe(true)
  43. await page.screenshot({ path: testInfo.outputPath('bot-project-memory-mobile.png') })
  44. await panel.getByRole('button', { name: '收起', exact: true }).click()
  45. await edited.getByRole('button', { name: '遗忘', exact: true }).click()
  46. await expect(panel.getByText('暂无记忆。可手动添加,也可在对话中说明需要长期记住的事实。')).toBeVisible()
  47. await panel.getByRole('button', { name: '用户记忆', exact: true }).click()
  48. await panel.getByRole('button', { name: '添加记忆', exact: true }).click()
  49. await panel.getByRole('textbox', { name: '事实', exact: true }).fill('用户偏好简洁的中文说明')
  50. await panel.getByRole('button', { name: '保存', exact: true }).click()
  51. await expect(panel.locator('article').filter({ hasText: '用户偏好简洁的中文说明' })).toBeVisible()
  52. const download = page.waitForEvent('download')
  53. await panel.getByRole('link', { name: '导出 Markdown', exact: true }).click()
  54. expect((await download).suggestedFilename()).toBe('bot-memory.md')
  55. await panel.getByRole('button', { name: '关闭记忆与协作', exact: true }).click()
  56. })
  57. test('simple group creation keeps chat lightweight and explicit delivery has editable collapsed details',async({page},testInfo)=>{
  58. await page.goto('/conversations')
  59. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  60. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  61. await page.getByRole('button',{name:'登录',exact:true}).click()
  62. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  63. for (const name of ['交付负责人','协作成员']) {
  64. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  65. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  66. const editor=page.getByRole('dialog',{name:'创建机器人'})
  67. await editor.getByRole('textbox',{name:'名称',exact:true}).fill(name)
  68. await expect(editor.getByRole('checkbox',{name:'允许组建团队',exact:true})).toHaveCount(0)
  69. await editor.getByRole('button',{name:'保存',exact:true}).click()
  70. await expect(page.getByRole('heading',{name,exact:true})).toBeVisible()
  71. }
  72. await page.reload()
  73. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  74. await page.getByRole('menuitem',{name:'新建群聊',exact:true}).click()
  75. const dialog=page.getByRole('dialog',{name:'创建群聊'})
  76. await expect(dialog.getByRole('combobox',{name:'协作方式',exact:true})).toBeHidden()
  77. await expect(dialog.getByText('软件开发团队',{exact:true})).toBeHidden()
  78. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('轻量协作群')
  79. await dialog.getByRole('checkbox',{name:'交付负责人',exact:true}).check()
  80. await dialog.getByRole('checkbox',{name:'协作成员',exact:true}).check()
  81. await expect(dialog.getByRole('combobox',{name:'负责人',exact:true})).toBeHidden()
  82. await dialog.getByRole('textbox',{name:'群规则(可选)',exact:true}).fill('回答简洁,结论注明来源。')
  83. await page.screenshot({path:testInfo.outputPath('simple-group-desktop.png')})
  84. await page.setViewportSize({width:390,height:844})
  85. await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  86. await expect(dialog.getByRole('button',{name:'保存',exact:true})).toBeInViewport()
  87. await page.screenshot({path:testInfo.outputPath('simple-group-mobile.png')})
  88. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  89. await expect(page.getByRole('heading',{name:'轻量协作群',exact:true})).toBeVisible()
  90. const id=new URL(page.url()).pathname.split('/').at(-1)!
  91. const snapshot=await (await page.request.get(`/api/app/conversations/${id}`)).json()
  92. expect(snapshot.tasks).toHaveLength(1)
  93. expect(snapshot.task.goal).toBeUndefined()
  94. expect(snapshot.assignments).toEqual([])
  95. await expect(page.locator('.task-plan')).toHaveCount(0)
  96. const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'})
  97. await composer.fill('简单讨论一下这个方案')
  98. await page.getByRole('button',{name:'发送消息',exact:true}).click()
  99. await expect(page.locator('.message--assistant')).not.toHaveCount(0)
  100. await expect(page.getByRole('button',{name:'交付目标',exact:true})).toBeEnabled()
  101. const before=await (await page.request.get(`/api/app/conversations/${id}`)).json()
  102. expect(before.task.goal).toBeUndefined()
  103. await page.getByRole('button',{name:'交付目标',exact:true}).click()
  104. await composer.fill('把方案做完并交付一份报告')
  105. const sent=page.waitForResponse(r=>r.request().method()==='POST'&&r.url().endsWith(`/api/app/conversations/${id}/messages`))
  106. await page.getByRole('button',{name:'发送消息',exact:true}).click()
  107. const response=await sent
  108. expect(response.request().postDataJSON().mode).toBe('goal')
  109. expect(response.ok(),await response.text()).toBe(true)
  110. await expect(page.getByRole('region',{name:'目标进度'})).toBeVisible()
  111. const details=page.locator('.task-plan details')
  112. await expect(details).not.toHaveAttribute('open')
  113. expect((await (await page.request.get(`/api/app/conversations/${id}`)).json()).tasks).toHaveLength(1)
  114. await details.locator('summary').click()
  115. await page.getByRole('button',{name:'调整验收要求',exact:true}).click()
  116. await page.getByRole('textbox',{name:'验收要求',exact:true}).fill('比较三种方案\n说明结论和依据')
  117. await page.getByRole('button',{name:'保存验收要求',exact:true}).click()
  118. await expect(details).toContainText('比较三种方案')
  119. await expect(page.getByRole('button',{name:'调整验收要求',exact:true})).toBeVisible()
  120. await page.screenshot({path:testInfo.outputPath('delivery-criteria-mobile.png')})
  121. await page.setViewportSize({width:1280,height:900})
  122. await page.emulateMedia({colorScheme:'light'})
  123. await details.locator('summary').click()
  124. await page.screenshot({path:testInfo.outputPath('delivery-progress-desktop.png')})
  125. await page.getByRole('button',{name:'停止任务',exact:true}).click()
  126. await expect(page.locator('.task-plan header')).toContainText('已停止')
  127. })
  128. test('new bots can team by default and the permission checkboxes are gone', async ({page}) => {
  129. await page.goto('/conversations')
  130. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  131. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  132. await page.getByRole('button',{name:'登录',exact:true}).click()
  133. await expect(page.getByRole('heading',{name:'还没有聊天'})).toBeVisible()
  134. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  135. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  136. const dialog=page.getByRole('dialog')
  137. await expect(dialog.getByRole('checkbox',{name:'允许组建团队',exact:true})).toHaveCount(0)
  138. await expect(dialog.getByRole('checkbox',{name:'允许 Bot 协作',exact:true})).toHaveCount(0)
  139. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('组队老板')
  140. const saved=page.waitForResponse(r=>r.request().method()==='POST' && r.url().endsWith('/api/app/agents'))
  141. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  142. const response=await saved
  143. expect(response.ok(),await response.text()).toBe(true)
  144. expect((await response.json()).agent.canManageTeam).toBe(true)
  145. })
  146. test('task plans show owners and keep independent text and attachment drafts when switching tasks',async({page},testInfo)=>{
  147. await page.goto('/conversations')
  148. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  149. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  150. await page.getByRole('button',{name:'登录',exact:true}).click()
  151. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  152. const seeded=await page.request.post('/__test/task-plan?helper=1',{data:{}})
  153. expect(seeded.ok()).toBe(true)
  154. const ids=await seeded.json()
  155. await page.goto(`/conversations/${ids.sourceConversationId}`)
  156. await page.getByRole('link',{name:'打开任务',exact:true}).click()
  157. await expect(page).toHaveURL(new RegExp(`/conversations/${ids.conversationId}\\?taskId=${ids.firstTaskId}`))
  158. await page.locator('.task-plan details summary').click()
  159. await expect(page.locator('.task-plan')).toContainText('研究资料')
  160. await expect(page.locator('.task-plan')).toContainText('验收研究员')
  161. await expect(page.locator('.task-plan')).toContainText('临时助手')
  162. await page.screenshot({path:testInfo.outputPath('task-helper-active.png')})
  163. const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'})
  164. await composer.fill('调研任务的草稿')
  165. await page.locator('.composer-area input[type=file]').first().setInputFiles({name:'draft.txt',mimeType:'text/plain',buffer:Buffer.from('待发送附件')})
  166. await page.getByRole('combobox',{name:'当前话题'}).selectOption(ids.secondTaskId)
  167. await expect(composer).toHaveValue('')
  168. await composer.fill('独立任务的草稿')
  169. await page.getByRole('combobox',{name:'当前话题'}).selectOption(ids.firstTaskId)
  170. await expect(composer).toHaveValue('调研任务的草稿')
  171. await expect(page.locator('.composer-attachments')).toContainText('draft.txt')
  172. await page.screenshot({path:testInfo.outputPath('task-plan-drafts.png')})
  173. await page.getByRole('button',{name:'停止任务',exact:true}).click()
  174. await expect(page.locator('.task-plan')).toContainText('已停止')
  175. await expect(page.locator('.task-plan')).toContainText('已退役')
  176. if(!await page.getByText('验收研究员(临时) · 临时助手', {exact:false}).first().isVisible())await page.locator('.task-plan details summary').click()
  177. await page.screenshot({path:testInfo.outputPath('task-helper-retired.png')})
  178. })
  179. test('registers an execution node, downloads its private config and disables it',async({page},testInfo)=>{
  180. await page.goto('/conversations')
  181. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  182. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  183. await page.getByRole('button',{name:'登录',exact:true}).click()
  184. await page.locator('.desktop-sidebar .sidebar-account-switcher__main').click()
  185. await page.getByRole('menuitem',{name:'我的设置',exact:true}).click()
  186. const dialog=page.getByRole('dialog',{name:'我的设置'})
  187. await dialog.getByRole('button',{name:'Hermes 连接',exact:true}).click()
  188. await dialog.locator('details.runner-settings summary').click()
  189. const panel=dialog.getByRole('region',{name:'执行节点设置'})
  190. await panel.getByRole('textbox',{name:'节点名称',exact:true}).fill('隔离测试 Mac')
  191. await panel.getByRole('textbox',{name:'允许的 Profile'}).fill('default')
  192. await panel.getByRole('checkbox',{name:'启用隔离电脑 Worker'}).check()
  193. await panel.getByRole('textbox',{name:'兼容镜像 ID'}).fill('sha256:'+'a'.repeat(64))
  194. await panel.getByRole('textbox',{name:'Hermes Python 路径'}).fill('/fixture/venv/bin/python')
  195. await panel.getByRole('textbox',{name:'Hermes 源码目录'}).fill('/fixture/hermes-agent')
  196. await panel.getByRole('textbox',{name:'Hermes 配置目录'}).fill('/fixture/hermes')
  197. await panel.scrollIntoViewIfNeeded()
  198. await page.screenshot({path:testInfo.outputPath('runner-registration.png')})
  199. await panel.getByRole('button',{name:'注册节点',exact:true}).click()
  200. await expect(panel).toContainText('待连接')
  201. const downloaded=page.waitForEvent('download')
  202. await panel.getByRole('button',{name:'下载 Runner 配置',exact:true}).click()
  203. const download=await downloaded
  204. expect(download.suggestedFilename()).toBe('runner.json')
  205. const stream=await download.createReadStream();const chunks:Buffer[]=[]
  206. for await(const chunk of stream!)chunks.push(Buffer.from(chunk))
  207. const configuration=JSON.parse(Buffer.concat(chunks).toString())
  208. expect(configuration).toMatchObject({protocol:1,allowedProfiles:['default'],artifactRoots:[],computers:{network:'public-proxy',runtime:'docker',python:'/fixture/venv/bin/python',hermesHome:'/fixture/hermes'}})
  209. expect(configuration.token.length).toBeGreaterThan(32)
  210. const result=await (await page.request.get('/api/app/admin/runners')).json()
  211. expect(JSON.stringify(result)).not.toContain(configuration.token)
  212. expect(JSON.stringify(result)).not.toContain('tokenHash')
  213. await page.setViewportSize({width:390,height:844})
  214. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).scrollIntoViewIfNeeded()
  215. await page.screenshot({path:testInfo.outputPath('runner-registered-mobile.png')})
  216. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).click()
  217. await expect(panel).toContainText('已停用')
  218. expect((await (await page.request.get('/api/app/admin/runners')).json()).runners.find((r:any)=>r.id===configuration.runnerId).enabled).toBe(false)
  219. })
  220. test('configures the Agent computer in its chat panel and keeps preparation in app settings',async({page},testInfo)=>{
  221. await page.goto('/conversations')
  222. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture');await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass');await page.getByRole('button',{name:'登录',exact:true}).click()
  223. await page.locator('.desktop-sidebar .sidebar-create-trigger').click();await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  224. const editor=page.getByRole('dialog');await editor.getByRole('textbox',{name:'名称',exact:true}).fill('尚未配置的本地电脑')
  225. await expect(editor.getByRole('combobox',{name:'执行环境'})).toHaveCount(0)
  226. await editor.getByRole('button',{name:'保存',exact:true}).click()
  227. await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  228. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  229. const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox')
  230. await vmBox.check()
  231. await expect(vmBox).toBeChecked()
  232. await expect(dock.getByRole('button',{name:'设置本地虚拟机',exact:true})).toBeVisible()
  233. await page.screenshot({path:testInfo.outputPath('local-vm-unconfigured-agent.png')})
  234. })
  235. async function envCheck(page:any,label:string){
  236. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  237. const box=dock.locator('.env-check',{hasText:label}).getByRole('checkbox')
  238. return {box,dock}
  239. }
  240. async function openVmSettings(page:any){
  241. await page.locator('.desktop-sidebar').getByRole('button',{name:'工具',exact:true}).click()
  242. await page.getByRole('menuitem',{name:'Bot 设置',exact:true}).click()
  243. const settings=page.getByRole('dialog',{name:'Bot 设置',exact:true})
  244. await settings.getByRole('button',{name:'本地虚拟机',exact:true}).click()
  245. return settings
  246. }
  247. async function login(page:any,ready=false){
  248. await page.goto('/conversations');await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture');await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass');await page.getByRole('button',{name:'登录',exact:true}).click()
  249. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  250. await page.request.post('/__test/local-vm'+(ready?'?ready=1':''),{data:{}})
  251. }
  252. test('persists idle-stop settings and parallel environment toggles on desktop and mobile',async({page},testInfo)=>{
  253. await login(page,true)
  254. const settings=await openVmSettings(page)
  255. const timeout=settings.getByRole('combobox',{name:'虚拟机空闲停止时间',exact:true})
  256. await timeout.selectOption('30');await expect(timeout).toHaveValue('30')
  257. await timeout.selectOption('0');await expect(settings).toContainText('空闲时保持运行')
  258. expect((await(await page.request.get('/api/app/admin/local-vm')).json()).idleStopMinutes).toBe(0)
  259. await timeout.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('idle-stop-settings.png')})
  260. await settings.getByRole('button',{name:'关闭Bot 设置',exact:true}).click()
  261. await page.request.post('/__test/hybrid-runner',{data:{}})
  262. const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  263. await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  264. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  265. const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox'),hostBox=dock.locator('.env-check',{hasText:'本机控制'}).getByRole('checkbox')
  266. await vmBox.check();await expect(vmBox).toBeChecked()
  267. await hostBox.check();await expect(hostBox).toBeChecked()
  268. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true,desktop:true},computer:'vm',execution:'computer'})
  269. await expect(dock).toContainText('永不自动停止')
  270. await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-desktop.png')})
  271. await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(vmBox).toBeChecked();await expect(hostBox).toBeChecked()
  272. await page.setViewportSize({width:390,height:844});await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('hybrid-mobile.png')})
  273. expect(await page.evaluate(()=>document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
  274. await hostBox.uncheck();await expect(dock.getByRole('checkbox',{name:'允许本机环境',exact:true})).toHaveCount(0)
  275. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true,desktop:false},computer:'vm',allowHostEnvironment:false})
  276. })
  277. test('keeps the parallel VM toggle through reload on desktop and phone',async({page},testInfo)=>{
  278. await login(page,true)
  279. await page.request.post('/__test/hybrid-runner',{data:{}})
  280. const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  281. await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  282. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  283. const vmBox=dock.locator('.env-check',{hasText:'本地虚拟机'}).getByRole('checkbox')
  284. await expect(dock.getByRole('combobox',{name:'本地虚拟机执行方式',exact:true})).toHaveCount(0)
  285. await expect(vmBox).toBeChecked()
  286. await vmBox.uncheck()
  287. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:false},computer:'off',execution:'profile'})
  288. await vmBox.check();await expect(vmBox).toBeChecked()
  289. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:true},computer:'vm',execution:'computer'})
  290. await vmBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-desktop.png')})
  291. await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(vmBox).toBeChecked()
  292. await page.setViewportSize({width:390,height:844});await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  293. await vmBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('profile-vm-phone.png')})
  294. expect(await page.evaluate(()=>document.documentElement.scrollWidth)).toBeLessThanOrEqual(390)
  295. await vmBox.uncheck()
  296. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{vm:false},computer:'off',execution:'profile'})
  297. })
  298. test('cloud stays selected while desktop runs in parallel from the checkbox panel',async({page},testInfo)=>{
  299. await login(page,true)
  300. const seed=await(await page.request.post('/__test/cloud-host-option',{data:{}})).json()
  301. await page.goto(`/conversations/${seed.conversationId}`);await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  302. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  303. const cloudBox=dock.locator('.env-check',{hasText:'云端 · Grok Bot'}).getByRole('checkbox'),hostBox=dock.locator('.env-check',{hasText:'本机控制'}).getByRole('checkbox')
  304. await expect(cloudBox).toBeChecked()
  305. await hostBox.check();await expect(cloudBox).toBeChecked();await expect(hostBox).toBeChecked()
  306. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{cloud:true,desktop:true},computer:'cloud',execution:'profile'})
  307. await hostBox.scrollIntoViewIfNeeded();await page.screenshot({path:testInfo.outputPath('cloud-host-option.png')})
  308. await page.reload();await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click();await expect(cloudBox).toBeChecked();await expect(hostBox).toBeChecked()
  309. await hostBox.uncheck()
  310. expect((await(await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===seed.agentId)).toMatchObject({envs:{cloud:true,desktop:false},allowHostEnvironment:false})
  311. })
  312. test('Local VM settings replace the image catalog and prepare the managed desktop',async({page},testInfo)=>{
  313. await login(page)
  314. await expect(page.getByRole('button',{name:'电脑与环境',exact:true})).toHaveCount(0)
  315. const settings=await openVmSettings(page)
  316. const panel=settings.getByRole('region',{name:'本地虚拟机设置'})
  317. await expect(panel).toContainText('隔离方式')
  318. await expect(panel.getByRole('button',{name:'上传并导入'})).toHaveCount(0)
  319. await panel.getByRole('button',{name:'准备所选镜像',exact:true}).click()
  320. await expect(panel).toContainText('虚拟机环境已就绪')
  321. await panel.getByRole('combobox',{name:'虚拟机数量上限'}).selectOption('3')
  322. await expect(panel.getByRole('combobox',{name:'虚拟机数量上限'})).toHaveValue('3')
  323. await panel.scrollIntoViewIfNeeded()
  324. await page.screenshot({path:testInfo.outputPath('local-vm-settings.png')})
  325. expect((await page.request.get('/api/app/admin/runners/11111111-1111-4111-8111-111111111111/images')).status()).toBe(404)
  326. })
  327. test('Agent computer appears as a right panel and opens an interactive desktop',async({page},testInfo)=>{
  328. await login(page)
  329. const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  330. await page.goto(`/conversations/${seed.conversationId}`)
  331. await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  332. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  333. await expect(dock).toBeVisible();await expect(dock).toContainText('本地虚拟机')
  334. await dock.getByRole('button',{name:'设置本地虚拟机',exact:true}).click()
  335. const settings=page.getByRole('dialog',{name:'Bot 设置'})
  336. await settings.getByRole('button',{name:'准备所选镜像',exact:true}).click()
  337. await settings.getByRole('button',{name:'关闭Bot 设置',exact:true}).click()
  338. await expect(dock.getByRole('button',{name:/创建.*的虚拟机/})).toBeVisible({timeout:12000})
  339. await dock.getByRole('button',{name:/创建.*的虚拟机/}).click()
  340. await expect(dock.getByRole('img')).toBeVisible()
  341. const bounds=await dock.boundingBox();expect(bounds!.x).toBeGreaterThan(500)
  342. await page.screenshot({path:testInfo.outputPath('agent-local-vm-panel.png')})
  343. expect((await(await page.request.get('/__test/computer')).json()).actions).toEqual([])
  344. await dock.getByRole('button',{name:'打开桌面',exact:true}).click()
  345. const viewer=page.getByRole('dialog',{name:'隔离电脑',exact:true})
  346. await expect(viewer).toContainText('人工控制中')
  347. await viewer.getByRole('button',{name:'键盘与操作',exact:true}).click()
  348. await viewer.getByRole('textbox',{name:'输入到电脑',exact:true}).fill('本地虚拟机交互验收')
  349. await viewer.getByRole('button',{name:'输入文字',exact:true}).click()
  350. await expect.poll(async()=>(await(await page.request.get('/__test/computer')).json()).actions).toEqual([{kind:'text',text:'本地虚拟机交互验收'}])
  351. await viewer.getByRole('img').dblclick()
  352. await expect.poll(async()=>(await(await page.request.get('/__test/computer')).json()).actions.filter((a:any)=>a.kind==='click')).toMatchObject([{kind:'click',button:'left',count:2}])
  353. await viewer.getByRole('button',{name:'交还并关闭',exact:true}).click()
  354. await dock.getByRole('button',{name:'停止虚拟机',exact:true}).click()
  355. await expect(dock.getByRole('button',{name:'启动虚拟机',exact:true})).toBeVisible()
  356. })
  357. test('two desktops are watch-only on opening and release before changing the controlled pane',async({page},testInfo)=>{
  358. await login(page,true)
  359. const first=await(await page.request.post('/__test/computer',{data:{}})).json()
  360. const second=await(await page.request.post('/__test/computer',{data:{}})).json()
  361. await page.goto(`/conversations/${first.conversationId}`)
  362. await page.getByRole('button',{name:'电脑与定时任务',exact:true}).click()
  363. const dock=page.getByRole('complementary',{name:'机器人电脑面板'})
  364. await expect(dock.getByRole('button',{name:'打开双桌面'})).toBeVisible()
  365. await dock.getByRole('button',{name:'打开双桌面'}).click()
  366. const workspace=page.getByRole('main',{name:'双桌面工作区'})
  367. await expect(workspace.locator('.pane')).toHaveCount(2)
  368. await workspace.getByRole('combobox',{name:'选择桌面 2'}).selectOption(second.agentId)
  369. await expect(workspace.locator('.pane').last().getByRole('img')).toBeVisible()
  370. await expect(workspace.getByRole('button',{name:'接管电脑',exact:true})).toHaveCount(2)
  371. await workspace.locator('.pane').first().getByRole('button',{name:'接管电脑',exact:true}).click()
  372. await expect(workspace.locator('.pane').first()).toContainText('人工控制中')
  373. await workspace.locator('.pane').last().getByRole('button',{name:'接管电脑',exact:true}).click()
  374. await expect(workspace.locator('.pane').first().getByRole('button',{name:'接管电脑',exact:true})).toBeVisible()
  375. await expect(workspace.locator('.pane').last()).toContainText('人工控制中')
  376. await page.screenshot({path:testInfo.outputPath('local-vm-two-desktops.png')})
  377. await workspace.getByRole('button',{name:'交还并关闭',exact:true}).click()
  378. await expect(workspace).toHaveCount(0)
  379. })