team-tools.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import { test, expect } from '@playwright/test'
  2. test('simple group creation keeps chat lightweight and explicit delivery has editable collapsed details',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.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  8. for (const [name,canManageTeam] of [['交付负责人',true],['协作成员',false]] as const) {
  9. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  10. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  11. const editor=page.getByRole('dialog',{name:'创建机器人'})
  12. await editor.getByRole('textbox',{name:'名称',exact:true}).fill(name)
  13. if(canManageTeam)await editor.getByRole('checkbox',{name:'允许组建团队',exact:true}).check()
  14. await editor.getByRole('button',{name:'保存',exact:true}).click()
  15. await expect(page.getByRole('heading',{name,exact:true})).toBeVisible()
  16. }
  17. await page.reload()
  18. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  19. await page.getByRole('menuitem',{name:'新建群聊',exact:true}).click()
  20. const dialog=page.getByRole('dialog',{name:'创建群聊'})
  21. await expect(dialog.getByRole('combobox',{name:'协作方式',exact:true})).toBeHidden()
  22. await expect(dialog.getByText('软件开发团队',{exact:true})).toBeHidden()
  23. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('轻量协作群')
  24. await dialog.getByRole('checkbox',{name:'交付负责人',exact:true}).check()
  25. await dialog.getByRole('checkbox',{name:'协作成员',exact:true}).check()
  26. await dialog.getByRole('combobox',{name:'负责人',exact:true}).selectOption({label:'交付负责人'})
  27. await dialog.getByRole('textbox',{name:'群规则(可选)',exact:true}).fill('回答简洁,结论注明来源。')
  28. await page.screenshot({path:testInfo.outputPath('simple-group-desktop.png')})
  29. await page.setViewportSize({width:390,height:844})
  30. await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  31. await expect(dialog.getByRole('button',{name:'保存',exact:true})).toBeInViewport()
  32. await page.screenshot({path:testInfo.outputPath('simple-group-mobile.png')})
  33. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  34. await expect(page.getByRole('heading',{name:'轻量协作群',exact:true})).toBeVisible()
  35. const id=new URL(page.url()).pathname.split('/').at(-1)!
  36. const snapshot=await (await page.request.get(`/api/app/conversations/${id}`)).json()
  37. expect(snapshot.tasks).toHaveLength(1)
  38. expect(snapshot.task.goal).toBeUndefined()
  39. expect(snapshot.assignments).toEqual([])
  40. await expect(page.locator('.task-plan')).toHaveCount(0)
  41. const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'})
  42. await composer.fill('简单讨论一下这个方案')
  43. await page.getByRole('button',{name:'发送消息',exact:true}).click()
  44. await expect(page.locator('.message--assistant')).not.toHaveCount(0)
  45. await expect(page.getByRole('button',{name:'交付目标',exact:true})).toBeEnabled()
  46. const before=await (await page.request.get(`/api/app/conversations/${id}`)).json()
  47. expect(before.task.goal).toBeUndefined()
  48. await page.getByRole('button',{name:'交付目标',exact:true}).click()
  49. await composer.fill('把方案做完并交付一份报告')
  50. const sent=page.waitForResponse(r=>r.request().method()==='POST'&&r.url().endsWith(`/api/app/conversations/${id}/messages`))
  51. await page.getByRole('button',{name:'发送消息',exact:true}).click()
  52. const response=await sent
  53. expect(response.request().postDataJSON().mode).toBe('goal')
  54. expect(response.ok(),await response.text()).toBe(true)
  55. await expect(page.getByRole('region',{name:'目标进度'})).toBeVisible()
  56. const details=page.locator('.task-plan details')
  57. await expect(details).not.toHaveAttribute('open')
  58. expect((await (await page.request.get(`/api/app/conversations/${id}`)).json()).tasks).toHaveLength(1)
  59. await details.locator('summary').click()
  60. await page.getByRole('button',{name:'调整验收要求',exact:true}).click()
  61. await page.getByRole('textbox',{name:'验收要求',exact:true}).fill('比较三种方案\n说明结论和依据')
  62. await page.getByRole('button',{name:'保存验收要求',exact:true}).click()
  63. await expect(details).toContainText('比较三种方案')
  64. await expect(page.getByRole('button',{name:'调整验收要求',exact:true})).toBeVisible()
  65. await page.screenshot({path:testInfo.outputPath('delivery-criteria-mobile.png')})
  66. await page.setViewportSize({width:1280,height:900})
  67. await page.emulateMedia({colorScheme:'light'})
  68. await details.locator('summary').click()
  69. await page.screenshot({path:testInfo.outputPath('delivery-progress-desktop.png')})
  70. await page.getByRole('button',{name:'停止任务',exact:true}).click()
  71. await expect(page.locator('.task-plan header')).toContainText('已停止')
  72. })
  73. test('team permission defaults off, saves, reloads and revokes in the Agent editor', async ({page},testInfo) => {
  74. await page.goto('/conversations')
  75. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  76. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  77. await page.getByRole('button',{name:'登录',exact:true}).click()
  78. await expect(page.getByRole('heading',{name:'还没有聊天'})).toBeVisible()
  79. await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
  80. await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  81. const dialog=page.getByRole('dialog'), permission=dialog.getByRole('checkbox',{name:'允许组建团队',exact:true})
  82. await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('组队老板')
  83. await expect(permission).not.toBeChecked()
  84. await permission.check()
  85. await permission.scrollIntoViewIfNeeded()
  86. await page.screenshot({path:testInfo.outputPath('team-permission-desktop.png')})
  87. const saved=page.waitForResponse(r=>r.request().method()==='POST' && r.url().endsWith('/api/app/agents'))
  88. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  89. const response=await saved
  90. expect(response.ok(),await response.text()).toBe(true)
  91. const agent=(await response.json()).agent
  92. expect(agent.canManageTeam).toBe(true)
  93. await page.reload()
  94. await expect(page.getByRole('heading',{name:'组队老板',exact:true})).toBeVisible()
  95. const edit=page.getByRole('button',{name:'聊天设置',exact:true})
  96. await edit.click()
  97. await expect(permission).toBeChecked()
  98. await page.setViewportSize({width:375,height:812})
  99. await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
  100. await permission.scrollIntoViewIfNeeded()
  101. await page.screenshot({path:testInfo.outputPath('team-permission-mobile.png')})
  102. await permission.uncheck()
  103. const revoked=page.waitForResponse(r=>r.request().method()==='PATCH' && r.url().endsWith(`/api/app/agents/${agent.id}`))
  104. await dialog.getByRole('button',{name:'保存',exact:true}).click()
  105. expect((await (await revoked).json()).agent.canManageTeam).toBe(false)
  106. expect((await (await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===agent.id).canManageTeam).toBe(false)
  107. })
  108. test('task plans show owners and keep independent text and attachment drafts when switching tasks',async({page},testInfo)=>{
  109. await page.goto('/conversations')
  110. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  111. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  112. await page.getByRole('button',{name:'登录',exact:true}).click()
  113. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  114. const seeded=await page.request.post('/__test/task-plan?helper=1',{data:{}})
  115. expect(seeded.ok()).toBe(true)
  116. const ids=await seeded.json()
  117. await page.goto(`/conversations/${ids.sourceConversationId}`)
  118. await page.getByRole('link',{name:'打开任务',exact:true}).click()
  119. await expect(page).toHaveURL(new RegExp(`/conversations/${ids.conversationId}\\?taskId=${ids.firstTaskId}`))
  120. await page.locator('.task-plan details summary').click()
  121. await expect(page.locator('.task-plan')).toContainText('研究资料')
  122. await expect(page.locator('.task-plan')).toContainText('验收研究员')
  123. await expect(page.locator('.task-plan')).toContainText('临时助手')
  124. await page.screenshot({path:testInfo.outputPath('task-helper-active.png')})
  125. const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'})
  126. await composer.fill('调研任务的草稿')
  127. await page.locator('.composer-area input[type=file]').first().setInputFiles({name:'draft.txt',mimeType:'text/plain',buffer:Buffer.from('待发送附件')})
  128. await page.getByRole('combobox',{name:'当前话题'}).selectOption(ids.secondTaskId)
  129. await expect(composer).toHaveValue('')
  130. await composer.fill('独立任务的草稿')
  131. await page.getByRole('combobox',{name:'当前话题'}).selectOption(ids.firstTaskId)
  132. await expect(composer).toHaveValue('调研任务的草稿')
  133. await expect(page.locator('.composer-attachments')).toContainText('draft.txt')
  134. await page.screenshot({path:testInfo.outputPath('task-plan-drafts.png')})
  135. await page.getByRole('button',{name:'停止任务',exact:true}).click()
  136. await expect(page.locator('.task-plan')).toContainText('已停止')
  137. await expect(page.locator('.task-plan')).toContainText('已退役')
  138. if(!await page.getByText('验收研究员(临时) · 临时助手', {exact:false}).first().isVisible())await page.locator('.task-plan details summary').click()
  139. await page.screenshot({path:testInfo.outputPath('task-helper-retired.png')})
  140. })
  141. test('registers an execution node, downloads its private config and disables it',async({page},testInfo)=>{
  142. await page.goto('/conversations')
  143. await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
  144. await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
  145. await page.getByRole('button',{name:'登录',exact:true}).click()
  146. await page.getByRole('button',{name:/^(打开设置中心|设置与模式)$/}).first().click()
  147. if(await page.getByRole('menuitem',{name:'进入设置',exact:true}).isVisible())await page.getByRole('menuitem',{name:'进入设置',exact:true}).click()
  148. const dialog=page.getByRole('dialog',{name:'设置中心'})
  149. await dialog.getByRole('button',{name:'Hermes 连接',exact:true}).click()
  150. await dialog.locator('details.runner-settings summary').click()
  151. const panel=dialog.getByRole('region',{name:'执行节点设置'})
  152. await panel.getByRole('textbox',{name:'节点名称',exact:true}).fill('隔离测试 Mac')
  153. await panel.getByRole('textbox',{name:'允许的 Profile'}).fill('default')
  154. await panel.getByRole('checkbox',{name:'启用隔离电脑 Worker'}).check()
  155. await panel.getByRole('textbox',{name:'兼容镜像 ID'}).fill('sha256:'+'a'.repeat(64))
  156. await panel.getByRole('textbox',{name:'Hermes Python 路径'}).fill('/fixture/venv/bin/python')
  157. await panel.getByRole('textbox',{name:'Hermes 源码目录'}).fill('/fixture/hermes-agent')
  158. await panel.getByRole('textbox',{name:'Hermes 配置目录'}).fill('/fixture/hermes')
  159. await panel.scrollIntoViewIfNeeded()
  160. await page.screenshot({path:testInfo.outputPath('runner-registration.png')})
  161. await panel.getByRole('button',{name:'注册节点',exact:true}).click()
  162. await expect(panel).toContainText('待连接')
  163. const downloaded=page.waitForEvent('download')
  164. await panel.getByRole('button',{name:'下载 Runner 配置',exact:true}).click()
  165. const download=await downloaded
  166. expect(download.suggestedFilename()).toBe('runner.json')
  167. const stream=await download.createReadStream();const chunks:Buffer[]=[]
  168. for await(const chunk of stream!)chunks.push(Buffer.from(chunk))
  169. const configuration=JSON.parse(Buffer.concat(chunks).toString())
  170. expect(configuration).toMatchObject({protocol:1,allowedProfiles:['default'],artifactRoots:[],computers:{network:'public-proxy',runtime:'docker',python:'/fixture/venv/bin/python',hermesHome:'/fixture/hermes'}})
  171. expect(configuration.token.length).toBeGreaterThan(32)
  172. const result=await (await page.request.get('/api/app/admin/runners')).json()
  173. expect(JSON.stringify(result)).not.toContain(configuration.token)
  174. expect(JSON.stringify(result)).not.toContain('tokenHash')
  175. await page.setViewportSize({width:390,height:844})
  176. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).scrollIntoViewIfNeeded()
  177. await page.screenshot({path:testInfo.outputPath('runner-registered-mobile.png')})
  178. await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).click()
  179. await expect(panel).toContainText('已停用')
  180. expect((await (await page.request.get('/api/app/admin/runners')).json()).runners.find((r:any)=>r.id===configuration.runnerId).enabled).toBe(false)
  181. })
  182. test('configures the Agent computer in its chat panel and keeps preparation in app settings',async({page},testInfo)=>{
  183. await page.goto('/conversations')
  184. 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()
  185. await page.locator('.desktop-sidebar .sidebar-create-trigger').click();await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
  186. const editor=page.getByRole('dialog');await editor.getByRole('textbox',{name:'名称',exact:true}).fill('尚未配置的本地电脑')
  187. await expect(editor.getByRole('combobox',{name:'执行环境'})).toHaveCount(0)
  188. await editor.getByRole('button',{name:'保存',exact:true}).click()
  189. await page.getByRole('button',{name:'电脑',exact:true}).click()
  190. const dock=page.getByRole('complementary',{name:'Agent 电脑面板'})
  191. const vmTab=dock.getByRole('tab',{name:'本地虚拟机',exact:true})
  192. await vmTab.click()
  193. await expect(vmTab).toHaveAttribute('aria-selected','true')
  194. await expect(dock.getByRole('button',{name:'设置本地虚拟机',exact:true})).toBeVisible()
  195. await page.screenshot({path:testInfo.outputPath('local-vm-unconfigured-agent.png')})
  196. })
  197. async function login(page:any,ready=false){
  198. 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()
  199. await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
  200. await page.request.post('/__test/local-vm'+(ready?'?ready=1':''),{data:{}})
  201. }
  202. test('Local VM settings replace the image catalog and prepare the managed desktop',async({page},testInfo)=>{
  203. await login(page)
  204. await expect(page.getByRole('button',{name:'电脑与环境',exact:true})).toHaveCount(0)
  205. await page.getByRole('button',{name:'设置与模式',exact:true}).first().click()
  206. await page.getByRole('menuitem',{name:'进入设置',exact:true}).click()
  207. const settings=page.getByRole('dialog',{name:'设置中心'})
  208. await settings.getByRole('button',{name:'本地虚拟机',exact:true}).click()
  209. const panel=settings.getByRole('region',{name:'本地虚拟机设置'})
  210. await expect(panel).toContainText('隔离方式')
  211. await expect(panel.getByRole('button',{name:'上传并导入'})).toHaveCount(0)
  212. await panel.getByRole('button',{name:'准备本地虚拟机',exact:true}).click()
  213. await expect(panel).toContainText('虚拟机环境已就绪')
  214. await panel.getByRole('combobox',{name:'虚拟机数量上限'}).selectOption('3')
  215. await expect(panel.getByRole('combobox',{name:'虚拟机数量上限'})).toHaveValue('3')
  216. await panel.getByRole('heading',{name:'本地虚拟机',exact:true}).scrollIntoViewIfNeeded()
  217. await page.screenshot({path:testInfo.outputPath('local-vm-settings.png')})
  218. expect((await page.request.get('/api/app/admin/runners/11111111-1111-4111-8111-111111111111/images')).status()).toBe(404)
  219. })
  220. test('Agent computer appears as a right panel and opens an interactive desktop',async({page},testInfo)=>{
  221. await login(page)
  222. const seed=await(await page.request.post('/__test/computer?off=1',{data:{}})).json()
  223. await page.goto(`/conversations/${seed.conversationId}`)
  224. await page.getByRole('button',{name:'电脑',exact:true}).click()
  225. const dock=page.getByRole('complementary',{name:'Agent 电脑面板'})
  226. await expect(dock).toBeVisible();await expect(dock).toContainText('本地虚拟机')
  227. await dock.getByRole('button',{name:'设置本地虚拟机',exact:true}).click()
  228. const settings=page.getByRole('dialog',{name:'设置中心'})
  229. await settings.getByRole('button',{name:'准备本地虚拟机',exact:true}).click()
  230. await settings.getByRole('button',{name:'关闭设置中心',exact:true}).click()
  231. await expect(dock.getByRole('button',{name:/创建.*的虚拟机/})).toBeVisible({timeout:12000})
  232. await dock.getByRole('button',{name:/创建.*的虚拟机/}).click()
  233. await expect(dock.getByRole('img')).toBeVisible()
  234. const bounds=await dock.boundingBox();expect(bounds!.x).toBeGreaterThan(500)
  235. await page.screenshot({path:testInfo.outputPath('agent-local-vm-panel.png')})
  236. expect((await(await page.request.get('/__test/computer')).json()).actions).toEqual([])
  237. await dock.getByRole('button',{name:'打开桌面',exact:true}).click()
  238. const viewer=page.getByRole('dialog',{name:'隔离电脑',exact:true})
  239. await expect(viewer).toContainText('人工控制中')
  240. await viewer.getByRole('button',{name:'键盘与操作',exact:true}).click()
  241. await viewer.getByRole('textbox',{name:'输入到电脑',exact:true}).fill('本地虚拟机交互验收')
  242. await viewer.getByRole('button',{name:'输入文字',exact:true}).click()
  243. await expect.poll(async()=>(await(await page.request.get('/__test/computer')).json()).actions).toEqual([{kind:'text',text:'本地虚拟机交互验收'}])
  244. await viewer.getByRole('img').dblclick()
  245. 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}])
  246. await viewer.getByRole('button',{name:'交还并关闭',exact:true}).click()
  247. await dock.getByRole('button',{name:'停止虚拟机',exact:true}).click()
  248. await expect(dock.getByRole('button',{name:'启动虚拟机',exact:true})).toBeVisible()
  249. })
  250. test('two desktops are watch-only on opening and release before changing the controlled pane',async({page},testInfo)=>{
  251. await login(page,true)
  252. const first=await(await page.request.post('/__test/computer',{data:{}})).json()
  253. const second=await(await page.request.post('/__test/computer',{data:{}})).json()
  254. await page.goto(`/conversations/${first.conversationId}`)
  255. await page.getByRole('button',{name:'电脑',exact:true}).click()
  256. const dock=page.getByRole('complementary',{name:'Agent 电脑面板'})
  257. await expect(dock.getByRole('button',{name:'打开双桌面'})).toBeVisible()
  258. await dock.getByRole('button',{name:'打开双桌面'}).click()
  259. const workspace=page.getByRole('main',{name:'双桌面工作区'})
  260. await expect(workspace.locator('.pane')).toHaveCount(2)
  261. await workspace.getByRole('combobox',{name:'选择桌面 2'}).selectOption(second.agentId)
  262. await expect(workspace.locator('.pane').last().getByRole('img')).toBeVisible()
  263. await expect(workspace.getByRole('button',{name:'接管电脑',exact:true})).toHaveCount(2)
  264. await workspace.locator('.pane').first().getByRole('button',{name:'接管电脑',exact:true}).click()
  265. await expect(workspace.locator('.pane').first()).toContainText('人工控制中')
  266. await workspace.locator('.pane').last().getByRole('button',{name:'接管电脑',exact:true}).click()
  267. await expect(workspace.locator('.pane').first().getByRole('button',{name:'接管电脑',exact:true})).toBeVisible()
  268. await expect(workspace.locator('.pane').last()).toContainText('人工控制中')
  269. await page.screenshot({path:testInfo.outputPath('local-vm-two-desktops.png')})
  270. await workspace.getByRole('button',{name:'交还并关闭',exact:true}).click()
  271. await expect(workspace).toHaveCount(0)
  272. })