team-tools.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import { test, expect } from '@playwright/test'
  2. test('team permission defaults off, saves, reloads and revokes in the Agent editor', 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. await expect(page.getByRole('heading',{name:'还没有聊天'})).toBeVisible()
  8. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  9. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  10. const dialog=page.getByRole('dialog'), permission=dialog.getByRole('checkbox',{name:'允许组建团队',exact:true})
  11. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('组队老板')
  12. await expect(permission).not.toBeChecked()
  13. await permission.check()
  14. await permission.scrollIntoViewIfNeeded()
  15. await page.screenshot({path:testInfo.outputPath('team-permission-desktop.png')})
  16. const saved=page.waitForResponse(r=>r.request().method()==='POST' && r.url().endsWith('/api/app/agents'))
  17. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  18. const response=await saved
  19. expect(response.ok(),await response.text()).toBe(true)
  20. const agent=(await response.json()).agent
  21. expect(agent.canManageTeam).toBe(true)
  22. await page.reload()
  23. await expect(page.getByRole('heading',{name:'组队老板',exact:true})).toBeVisible()
  24. const edit=page.getByRole('button',{name:'聊天设置',exact:true})
  25. await edit.click()
  26. await expect(permission).toBeChecked()
  27. await page.setViewportSize({width:375,height:812})
  28. await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  29. await permission.scrollIntoViewIfNeeded()
  30. await page.screenshot({path:testInfo.outputPath('team-permission-mobile.png')})
  31. await permission.uncheck()
  32. const revoked=page.waitForResponse(r=>r.request().method()==='PATCH' && r.url().endsWith(`/api/app/agents/${agent.id}`))
  33. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  34. expect((await (await revoked).json()).agent.canManageTeam).toBe(false)
  35. expect((await (await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===agent.id).canManageTeam).toBe(false)
  36. })
  37. test('task plans show owners and keep independent text and attachment drafts when switching tasks',async({page},testInfo)=>{
  38. await page.goto('/conversations')
  39. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  40. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  41. await page.getByRole('button',{name:'登录',exact:true}).click()
  42. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  43. const seeded=await page.request.post('/__test/task-plan?helper=1',{data:{}})
  44. expect(seeded.ok()).toBe(true)
  45. const ids=await seeded.json()
  46. await page.goto(`/conversations/${ids.sourceConversationId}`)
  47. await page.getByRole('link',{name:'打开任务',exact:true}).click()
  48. await expect(page).toHaveURL(new RegExp(`/conversations/${ids.conversationId}\\?taskId=${ids.firstTaskId}`))
  49. await page.locator('details.task-plan summary').click()
  50. await expect(page.locator('.task-plan')).toContainText('研究资料')
  51. await expect(page.locator('.task-plan')).toContainText('验收研究员')
  52. await expect(page.locator('.task-plan')).toContainText('临时助手')
  53. await page.screenshot({path:testInfo.outputPath('task-helper-active.png')})
  54. const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'})
  55. await composer.fill('调研任务的草稿')
  56. await page.locator('.composer-area input[type=file]').first().setInputFiles({name:'draft.txt',mimeType:'text/plain',buffer:Buffer.from('待发送附件')})
  57. await page.getByRole('combobox',{name:'当前任务'}).selectOption(ids.secondTaskId)
  58. await expect(composer).toHaveValue('')
  59. await composer.fill('独立任务的草稿')
  60. await page.getByRole('combobox',{name:'当前任务'}).selectOption(ids.firstTaskId)
  61. await expect(composer).toHaveValue('调研任务的草稿')
  62. await expect(page.locator('.composer-attachments')).toContainText('draft.txt')
  63. await page.screenshot({path:testInfo.outputPath('task-plan-drafts.png')})
  64. if(!await page.getByRole('button',{name:'停止任务',exact:true}).isVisible())await page.locator('details.task-plan summary').click()
  65. await page.getByRole('button',{name:'停止任务',exact:true}).click()
  66. await expect(page.locator('.task-plan')).toContainText('已停止')
  67. await expect(page.locator('.task-plan')).toContainText('已退役')
  68. if(!await page.getByText('验收研究员(临时) · 临时助手', {exact:false}).first().isVisible())await page.locator('details.task-plan summary').click()
  69. await page.screenshot({path:testInfo.outputPath('task-helper-retired.png')})
  70. })
  71. test('registers an execution node, downloads its private config and disables it',async({page},testInfo)=>{
  72. await page.goto('/conversations')
  73. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  74. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  75. await page.getByRole('button',{name:'登录',exact:true}).click()
  76. await page.getByRole('button',{name:/^(打开设置中心|设置与模式)$/}).first().click()
  77. if(await page.getByRole('menuitem',{name:'进入设置',exact:true}).isVisible())await page.getByRole('menuitem',{name:'进入设置',exact:true}).click()
  78. const dialog=page.getByRole('dialog',{name:'设置中心'})
  79. await dialog.getByRole('button',{name:'Hermes 连接',exact:true}).click()
  80. await dialog.locator('details.runner-settings summary').click()
  81. const panel=dialog.getByRole('region',{name:'执行节点设置'})
  82. await panel.getByRole('textbox',{name:'节点名称',exact:true}).fill('隔离测试 Mac')
  83. await panel.getByRole('textbox',{name:'允许的 Profile'}).fill('default')
  84. await panel.getByRole('checkbox',{name:'启用隔离电脑 Worker'}).check()
  85. await panel.getByRole('textbox',{name:'兼容镜像 ID'}).fill('sha256:'+'a'.repeat(64))
  86. await panel.getByRole('textbox',{name:'Hermes Python 路径'}).fill('/fixture/venv/bin/python')
  87. await panel.getByRole('textbox',{name:'Hermes 源码目录'}).fill('/fixture/hermes-agent')
  88. await panel.getByRole('textbox',{name:'Hermes 配置目录'}).fill('/fixture/hermes')
  89. await panel.scrollIntoViewIfNeeded()
  90. await page.screenshot({path:testInfo.outputPath('runner-registration.png')})
  91. await panel.getByRole('button',{name:'注册节点',exact:true}).click()
  92. await expect(panel).toContainText('待连接')
  93. const downloaded=page.waitForEvent('download')
  94. await panel.getByRole('button',{name:'下载 Runner 配置',exact:true}).click()
  95. const download=await downloaded
  96. expect(download.suggestedFilename()).toBe('runner.json')
  97. const stream=await download.createReadStream();const chunks:Buffer[]=[]
  98. for await(const chunk of stream!)chunks.push(Buffer.from(chunk))
  99. const configuration=JSON.parse(Buffer.concat(chunks).toString())
  100. expect(configuration).toMatchObject({protocol:1,allowedProfiles:['default'],artifactRoots:[],computers:{network:'public-proxy',runtime:'docker',python:'/fixture/venv/bin/python',hermesHome:'/fixture/hermes'}})
  101. expect(configuration.token.length).toBeGreaterThan(32)
  102. const result=await (await page.request.get('/api/app/admin/runners')).json()
  103. expect(JSON.stringify(result)).not.toContain(configuration.token)
  104. expect(JSON.stringify(result)).not.toContain('tokenHash')
  105. await page.setViewportSize({width:390,height:844})
  106. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).scrollIntoViewIfNeeded()
  107. await page.screenshot({path:testInfo.outputPath('runner-registered-mobile.png')})
  108. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).click()
  109. await expect(panel).toContainText('已停用')
  110. expect((await (await page.request.get('/api/app/admin/runners')).json()).runners.find((r:any)=>r.id===configuration.runnerId).enabled).toBe(false)
  111. })
  112. test('explains isolated execution and keeps the edit form intact when a computer Runner is missing',async({page},testInfo)=>{
  113. await page.goto('/conversations')
  114. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  115. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  116. await page.getByRole('button',{name:'登录',exact:true}).click()
  117. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  118. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  119. const dialog=page.getByRole('dialog')
  120. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('浏览器隔离成员')
  121. await dialog.getByRole('combobox',{name:'执行环境',exact:true}).selectOption('computer')
  122. await expect(dialog).toContainText('联网范围由执行节点的配置决定')
  123. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  124. await expect(dialog.getByText(/隔离电脑需要连接执行节点/)).toBeVisible()
  125. await expect(dialog).toBeVisible()
  126. await expect(dialog.getByRole('combobox',{name:'执行环境',exact:true})).toHaveValue('computer')
  127. await page.setViewportSize({width:390,height:844})
  128. await dialog.getByRole('combobox',{name:'执行环境',exact:true}).scrollIntoViewIfNeeded()
  129. await page.screenshot({path:testInfo.outputPath('computer-execution-mobile.png')})
  130. })
  131. test('computer viewing is read-only until takeover and returns control with a user note',async({page},testInfo)=>{
  132. await page.goto('/conversations')
  133. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  134. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  135. await page.getByRole('button',{name:'登录',exact:true}).click()
  136. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  137. const seeded=await page.request.post('/__test/computer',{data:{}});expect(seeded.ok(),await seeded.text()).toBe(true)
  138. const fixture=await seeded.json();expect(fixture.conversationId).toBeTruthy()
  139. await page.goto(`/conversations/${fixture.conversationId}`)
  140. await page.getByRole('button',{name:'电脑',exact:true}).click()
  141. const panel=page.getByRole('dialog',{name:'隔离电脑',exact:true}),screen=panel.getByRole('img',{name:'隔离电脑当前画面'})
  142. await expect(screen).toBeVisible()
  143. await screen.click()
  144. expect((await (await page.request.get('/__test/computer')).json()).actions).toEqual([])
  145. await panel.getByRole('button',{name:'接管电脑',exact:true}).click()
  146. await expect(panel).toContainText('人工控制中')
  147. await panel.getByRole('textbox',{name:'输入到电脑',exact:true}).fill('人工输入验收')
  148. await panel.getByRole('button',{name:'输入文字',exact:true}).click()
  149. await expect.poll(async()=>(await (await page.request.get('/__test/computer')).json()).actions).toEqual([{kind:'text',text:'人工输入验收'}])
  150. await panel.getByRole('textbox',{name:'交还说明',exact:true}).fill('已完成设置,请继续任务')
  151. await page.setViewportSize({width:390,height:844})
  152. await panel.getByRole('button',{name:'交还 Agent 并继续',exact:true}).scrollIntoViewIfNeeded()
  153. await page.screenshot({path:testInfo.outputPath('computer-panel-mobile.png')})
  154. await panel.getByRole('button',{name:'交还 Agent 并继续',exact:true}).click()
  155. await expect(panel.getByRole('button',{name:'接管电脑',exact:true})).toBeVisible()
  156. await panel.getByRole('button',{name:'关闭',exact:true}).click()
  157. await expect(panel).not.toBeVisible()
  158. })
  159. test('trusted shared computers require an explicit data-sharing grant and can be detached',async({page},testInfo)=>{
  160. await page.goto('/conversations')
  161. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  162. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  163. await page.getByRole('button',{name:'登录',exact:true}).click()
  164. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  165. const seeded=await page.request.post('/__test/computer?shared=1',{data:{}});expect(seeded.ok(),await seeded.text()).toBe(true)
  166. const fixture=await seeded.json();await page.goto(`/conversations/${fixture.conversationId}`)
  167. await page.getByRole('button',{name:'电脑',exact:true}).click()
  168. const panel=page.getByRole('dialog',{name:'隔离电脑',exact:true})
  169. await panel.getByRole('button',{name:'电脑共享设置',exact:true}).click()
  170. const create=panel.getByRole('button',{name:'创建可信共享电脑',exact:true})
  171. await expect(create).toBeDisabled()
  172. await panel.getByRole('checkbox',{name:'我确认这些成员可以访问同一工作区和浏览器登录资料'}).check()
  173. await create.click()
  174. await expect(panel).toContainText('共享电脑:团队共享电脑')
  175. await page.setViewportSize({width:390,height:844})
  176. await panel.getByRole('button',{name:'电脑共享设置',exact:true}).click()
  177. await panel.getByRole('button',{name:'解除这台电脑的共享',exact:true}).scrollIntoViewIfNeeded()
  178. await page.screenshot({path:testInfo.outputPath('shared-computer-mobile.png')})
  179. await panel.getByRole('button',{name:'解除这台电脑的共享',exact:true}).click()
  180. await expect(panel).not.toContainText('共享电脑:团队共享电脑')
  181. const agents=(await (await page.request.get('/api/app/agents')).json()).agents
  182. expect(agents.find((agent:any)=>agent.id===fixture.agentId).computerEnvironmentId).toBeUndefined()
  183. })