import { test, expect } from '@playwright/test' test('team permission defaults off, saves, reloads and revokes in the Agent editor', async ({page},testInfo) => { 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() await expect(page.getByRole('heading',{name:'还没有聊天'})).toBeVisible() await page.locator('.desktop-sidebar .sidebar-create-trigger').click() await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click() const dialog=page.getByRole('dialog'), permission=dialog.getByRole('checkbox',{name:'允许组建团队',exact:true}) await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('组队老板') await expect(permission).not.toBeChecked() await permission.check() await permission.scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('team-permission-desktop.png')}) const saved=page.waitForResponse(r=>r.request().method()==='POST' && r.url().endsWith('/api/app/agents')) await dialog.getByRole('button',{name:'保存',exact:true}).click() const response=await saved expect(response.ok(),await response.text()).toBe(true) const agent=(await response.json()).agent expect(agent.canManageTeam).toBe(true) await page.reload() await expect(page.getByRole('heading',{name:'组队老板',exact:true})).toBeVisible() const edit=page.getByRole('button',{name:'聊天设置',exact:true}) await edit.click() await expect(permission).toBeChecked() await page.setViewportSize({width:375,height:812}) await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'}) await permission.scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('team-permission-mobile.png')}) await permission.uncheck() const revoked=page.waitForResponse(r=>r.request().method()==='PATCH' && r.url().endsWith(`/api/app/agents/${agent.id}`)) await dialog.getByRole('button',{name:'保存',exact:true}).click() expect((await (await revoked).json()).agent.canManageTeam).toBe(false) expect((await (await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===agent.id).canManageTeam).toBe(false) }) test('task plans show owners and keep independent text and attachment drafts when switching tasks',async({page},testInfo)=>{ 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() await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible() const seeded=await page.request.post('/__test/task-plan?helper=1',{data:{}}) expect(seeded.ok()).toBe(true) const ids=await seeded.json() await page.goto(`/conversations/${ids.sourceConversationId}`) await page.getByRole('link',{name:'打开任务',exact:true}).click() await expect(page).toHaveURL(new RegExp(`/conversations/${ids.conversationId}\\?taskId=${ids.firstTaskId}`)) await page.locator('details.task-plan summary').click() await expect(page.locator('.task-plan')).toContainText('研究资料') await expect(page.locator('.task-plan')).toContainText('验收研究员') await expect(page.locator('.task-plan')).toContainText('临时助手') await page.screenshot({path:testInfo.outputPath('task-helper-active.png')}) const composer=page.getByRole('textbox',{name:'输入消息,Enter 发送,Shift + Enter 换行'}) await composer.fill('调研任务的草稿') await page.locator('.composer-area input[type=file]').first().setInputFiles({name:'draft.txt',mimeType:'text/plain',buffer:Buffer.from('待发送附件')}) await page.getByRole('combobox',{name:'当前任务'}).selectOption(ids.secondTaskId) await expect(composer).toHaveValue('') await composer.fill('独立任务的草稿') await page.getByRole('combobox',{name:'当前任务'}).selectOption(ids.firstTaskId) await expect(composer).toHaveValue('调研任务的草稿') await expect(page.locator('.composer-attachments')).toContainText('draft.txt') await page.screenshot({path:testInfo.outputPath('task-plan-drafts.png')}) if(!await page.getByRole('button',{name:'停止任务',exact:true}).isVisible())await page.locator('details.task-plan summary').click() await page.getByRole('button',{name:'停止任务',exact:true}).click() await expect(page.locator('.task-plan')).toContainText('已停止') await expect(page.locator('.task-plan')).toContainText('已退役') if(!await page.getByText('验收研究员(临时) · 临时助手', {exact:false}).first().isVisible())await page.locator('details.task-plan summary').click() await page.screenshot({path:testInfo.outputPath('task-helper-retired.png')}) }) test('registers an execution node, downloads its private config and disables it',async({page},testInfo)=>{ 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() await page.getByRole('button',{name:/^(打开设置中心|设置与模式)$/}).first().click() if(await page.getByRole('menuitem',{name:'进入设置',exact:true}).isVisible())await page.getByRole('menuitem',{name:'进入设置',exact:true}).click() const dialog=page.getByRole('dialog',{name:'设置中心'}) await dialog.getByRole('button',{name:'Hermes 连接',exact:true}).click() await dialog.locator('details.runner-settings summary').click() const panel=dialog.getByRole('region',{name:'执行节点设置'}) await panel.getByRole('textbox',{name:'节点名称',exact:true}).fill('隔离测试 Mac') await panel.getByRole('textbox',{name:'允许的 Profile'}).fill('default') await panel.getByRole('checkbox',{name:'启用隔离电脑 Worker'}).check() await panel.getByRole('textbox',{name:'兼容镜像 ID'}).fill('sha256:'+'a'.repeat(64)) await panel.getByRole('textbox',{name:'Hermes Python 路径'}).fill('/fixture/venv/bin/python') await panel.getByRole('textbox',{name:'Hermes 源码目录'}).fill('/fixture/hermes-agent') await panel.getByRole('textbox',{name:'Hermes 配置目录'}).fill('/fixture/hermes') await panel.scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('runner-registration.png')}) await panel.getByRole('button',{name:'注册节点',exact:true}).click() await expect(panel).toContainText('待连接') const downloaded=page.waitForEvent('download') await panel.getByRole('button',{name:'下载 Runner 配置',exact:true}).click() const download=await downloaded expect(download.suggestedFilename()).toBe('runner.json') const stream=await download.createReadStream();const chunks:Buffer[]=[] for await(const chunk of stream!)chunks.push(Buffer.from(chunk)) const configuration=JSON.parse(Buffer.concat(chunks).toString()) expect(configuration).toMatchObject({protocol:1,allowedProfiles:['default'],artifactRoots:[],computers:{network:'public-proxy',runtime:'docker',python:'/fixture/venv/bin/python',hermesHome:'/fixture/hermes'}}) expect(configuration.token.length).toBeGreaterThan(32) const result=await (await page.request.get('/api/app/admin/runners')).json() expect(JSON.stringify(result)).not.toContain(configuration.token) expect(JSON.stringify(result)).not.toContain('tokenHash') await page.setViewportSize({width:390,height:844}) await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('runner-registered-mobile.png')}) await panel.getByRole('button',{name:'停用 隔离测试 Mac'}).click() await expect(panel).toContainText('已停用') expect((await (await page.request.get('/api/app/admin/runners')).json()).runners.find((r:any)=>r.id===configuration.runnerId).enabled).toBe(false) }) test('explains isolated execution and keeps the edit form intact when a computer Runner is missing',async({page},testInfo)=>{ 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() await page.locator('.desktop-sidebar .sidebar-create-trigger').click() await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click() const dialog=page.getByRole('dialog') await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('浏览器隔离成员') await dialog.getByRole('combobox',{name:'执行环境',exact:true}).selectOption('computer') await expect(dialog).toContainText('联网范围由执行节点的配置决定') await dialog.getByRole('button',{name:'保存',exact:true}).click() await expect(dialog.getByText(/隔离电脑需要连接执行节点/)).toBeVisible() await expect(dialog).toBeVisible() await expect(dialog.getByRole('combobox',{name:'执行环境',exact:true})).toHaveValue('computer') await page.setViewportSize({width:390,height:844}) await dialog.getByRole('combobox',{name:'执行环境',exact:true}).scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('computer-execution-mobile.png')}) }) test('computer viewing is read-only until takeover and returns control with a user note',async({page},testInfo)=>{ 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() await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible() const seeded=await page.request.post('/__test/computer',{data:{}});expect(seeded.ok(),await seeded.text()).toBe(true) const fixture=await seeded.json();expect(fixture.conversationId).toBeTruthy() await page.goto(`/conversations/${fixture.conversationId}`) await page.getByRole('button',{name:'电脑',exact:true}).click() const panel=page.getByRole('dialog',{name:'隔离电脑',exact:true}),screen=panel.getByRole('img',{name:'隔离电脑当前画面'}) await expect(screen).toBeVisible() await screen.click() expect((await (await page.request.get('/__test/computer')).json()).actions).toEqual([]) await panel.getByRole('button',{name:'接管电脑',exact:true}).click() await expect(panel).toContainText('人工控制中') await panel.getByRole('textbox',{name:'输入到电脑',exact:true}).fill('人工输入验收') await panel.getByRole('button',{name:'输入文字',exact:true}).click() await expect.poll(async()=>(await (await page.request.get('/__test/computer')).json()).actions).toEqual([{kind:'text',text:'人工输入验收'}]) await panel.getByRole('textbox',{name:'交还说明',exact:true}).fill('已完成设置,请继续任务') await page.setViewportSize({width:390,height:844}) await panel.getByRole('button',{name:'交还 Agent 并继续',exact:true}).scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('computer-panel-mobile.png')}) await panel.getByRole('button',{name:'交还 Agent 并继续',exact:true}).click() await expect(panel.getByRole('button',{name:'接管电脑',exact:true})).toBeVisible() await panel.getByRole('button',{name:'关闭',exact:true}).click() await expect(panel).not.toBeVisible() }) test('trusted shared computers require an explicit data-sharing grant and can be detached',async({page},testInfo)=>{ 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() await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible() const seeded=await page.request.post('/__test/computer?shared=1',{data:{}});expect(seeded.ok(),await seeded.text()).toBe(true) const fixture=await seeded.json();await page.goto(`/conversations/${fixture.conversationId}`) await page.getByRole('button',{name:'电脑',exact:true}).click() const panel=page.getByRole('dialog',{name:'隔离电脑',exact:true}) await panel.getByRole('button',{name:'电脑共享设置',exact:true}).click() const create=panel.getByRole('button',{name:'创建可信共享电脑',exact:true}) await expect(create).toBeDisabled() await panel.getByRole('checkbox',{name:'我确认这些成员可以访问同一工作区和浏览器登录资料'}).check() await create.click() await expect(panel).toContainText('共享电脑:团队共享电脑') await page.setViewportSize({width:390,height:844}) await panel.getByRole('button',{name:'电脑共享设置',exact:true}).click() await panel.getByRole('button',{name:'解除这台电脑的共享',exact:true}).scrollIntoViewIfNeeded() await page.screenshot({path:testInfo.outputPath('shared-computer-mobile.png')}) await panel.getByRole('button',{name:'解除这台电脑的共享',exact:true}).click() await expect(panel).not.toContainText('共享电脑:团队共享电脑') const agents=(await (await page.request.get('/api/app/agents')).json()).agents expect(agents.find((agent:any)=>agent.id===fixture.agentId).computerEnvironmentId).toBeUndefined() })