import { test, expect } from '@playwright/test' import { mkdirSync } from 'node:fs' test('a default installation creates and saves encrypted passwords through the settings UI', async ({ page }) => { const evidence='test-results/credential-vault-default';mkdirSync(evidence,{recursive:true}) const master='DUMMY-browser-vault-master',secret='DUMMY-browser-entry-secret' 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-account-switcher__main').click() await page.getByRole('menuitem',{name:'我的设置',exact:true}).click() await page.getByRole('button',{name:'密码管理',exact:true}).click() const panel=page.locator('.credential-vault') await expect(panel.getByText(/授权一次即可使用/)).toBeVisible() await panel.getByLabel('主密码',{exact:true}).fill(master) await panel.getByLabel('确认主密码',{exact:true}).fill(master) await panel.getByRole('button',{name:'创建密码库',exact:true}).click() await expect(panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true})).toBeVisible() await panel.getByLabel('主密码',{exact:true}).fill(master) await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click() await panel.getByRole('button',{name:'添加凭据',exact:true}).click() await panel.getByLabel('名称',{exact:true}).fill('浏览器测试网站') await panel.getByLabel('用户名',{exact:true}).fill('dummy-user') await panel.getByLabel('准确网站 origin',{exact:true}).fill('https://example.test') await panel.getByLabel('密码',{exact:true}).fill(secret) await panel.getByRole('button',{name:'加密保存',exact:true}).click() await expect(panel.locator('li strong').filter({hasText:'浏览器测试网站'})).toBeVisible() await expect(panel.getByRole('status').filter({hasText:'凭据已加密保存'})).toBeVisible() await expect(panel.getByRole('button',{name:'授权此 Bot 使用'})).toBeDisabled() expect(await panel.textContent()).not.toContain(secret) await panel.getByRole('button',{name:'添加凭据',exact:true}).click() await panel.getByRole('combobox',{name:/^用途/}).selectOption('ssh') await expect(panel.getByRole('combobox',{name:/^SSH 认证方式/})).toHaveValue('password') await panel.getByLabel('名称',{exact:true}).fill('浏览器测试 SSH 密码') await panel.getByLabel('用户名',{exact:true}).fill('dummy-ssh-user') await panel.getByLabel('SSH 主机',{exact:true}).fill('ssh.example.test') await expect(panel.getByLabel(/^服务器指纹/)).toBeHidden() await expect(panel.getByLabel(/^服务器指纹/)).not.toHaveAttribute('required') const sshPassword=panel.getByLabel(/^SSH 登录密码/) await expect(sshPassword).toHaveAttribute('type','password') await sshPassword.fill('DUMMY-browser-ssh-password') await expect(panel.getByText('授权 Bot 后,允许全部远程 SSH 命令并读取输出,不需要预先填写命令。权限由 SSH 账号决定。')).toBeVisible() await expect(panel.getByLabel('私钥口令(可选)',{exact:true})).toHaveCount(0) await page.screenshot({path:evidence+'/ssh-password-editor.png'}) await panel.getByRole('button',{name:'加密保存',exact:true}).click() const sshEntry=panel.locator('li').filter({hasText:'浏览器测试 SSH 密码'}) await expect(sshEntry).toContainText('密码登录') await expect(panel.getByRole('heading',{name:'授权 Bot 使用',exact:true})).toBeVisible() expect(await panel.textContent()).not.toContain('DUMMY-browser-ssh-password') await sshEntry.getByRole('button',{name:'编辑',exact:true}).click() await expect(sshPassword).toHaveValue('') await expect(panel.getByRole('combobox',{name:/^SSH 认证方式/})).toHaveValue('password') await panel.getByText('高级连接设置(可选)',{exact:true}).click() await expect(panel.getByLabel(/^服务器指纹/)).toHaveValue('') await expect(panel.getByLabel('端口',{exact:true})).toHaveValue('22') await panel.getByRole('button',{name:'加密保存',exact:true}).click() await expect(sshEntry).toContainText('密码登录') await page.screenshot({path:evidence+'/desktop-saved.png'}) await page.setViewportSize({width:375,height:812}) await expect(panel.getByRole('button',{name:'添加凭据',exact:true})).toBeVisible() expect(await panel.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true) await page.screenshot({path:evidence+'/mobile-saved.png'}) await page.setViewportSize({width:1440,height:1000}) await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click() await expect(panel.getByText('浏览器测试网站',{exact:true})).toHaveCount(0) await panel.getByLabel('主密码',{exact:true}).fill('DUMMY-wrong-master-password') await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click() await expect(panel.getByRole('alert')).toContainText('主密码错误') await expect(page.getByRole('textbox',{name:'账号',exact:true})).toHaveCount(0) await expect(panel.getByLabel('主密码',{exact:true})).toHaveValue('') await page.screenshot({path:evidence+'/wrong-master-stays-signed-in.png'}) }) test('UI approval survives port edits, closing settings and a full server process restart with real dummy SSH execution',async({page})=>{ const {mkdtempSync,rmSync}=await import('node:fs'),{tmpdir}=await import('node:os'),{join}=await import('node:path'),{spawn}=await import('node:child_process') const {sshFixture}=await import('../fixtures/credentialProtocols') const first=await sshFixture('password'),second=await sshFixture('password') first.state.execReplies.set('uname -s',{stdout:'Linux\n',stderr:''});second.state.execReplies.set('uname -s',{stdout:'Linux\n',stderr:''});second.state.execReplies.set('df -h / | head -n 2',{stdout:'Filesystem Size Used Avail Use% Mounted on\n/dev/test 100G 40G 60G 40% /\n',stderr:''}) const home=mkdtempSync(join(tmpdir(),'yaoyao-vault-browser-restart-')),base='http://127.0.0.1:18837',master='DUMMY-browser-persistent-master' let child:ReturnType|undefined,logs='' const start=async()=>{ child=spawn(process.execPath,['--import','tsx','tests/fixtures/workspace-server.ts'],{env:{...process.env,WORKSPACE_FIXTURE_HOME:home,WORKSPACE_FIXTURE_PORT:'18837',WORKSPACE_FIXTURE_UPSTREAM_PORT:'19137',WORKSPACE_FIXTURE_VAULT:'1'},stdio:['ignore','pipe','pipe']}) child.stdout!.on('data',c=>logs+=c);child.stderr!.on('data',c=>logs+=c) await expect.poll(async()=>{try{return (await fetch(base+'/healthz')).status}catch{return 0}},{timeout:20000,message:logs}).toBe(200) } const stop=async()=>{if(!child||child.exitCode!==null)return;await page.goto('about:blank').catch(()=>{});const current=child,exited=new Promise(r=>current.once('exit',()=>r()));current.kill('SIGTERM');const timeout=setTimeout(()=>current.kill('SIGKILL'),3000);await exited;clearTimeout(timeout);child=undefined} const login=async()=>{ await page.goto(base+'/conversations') if(await page.getByRole('textbox',{name:'账号',exact:true}).isVisible()){ 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-account-switcher__main')).toBeVisible() } const settings=async()=>{await page.locator('.desktop-sidebar .sidebar-account-switcher__main').click();await page.getByRole('menuitem',{name:'我的设置',exact:true}).click();await page.getByRole('button',{name:'密码管理',exact:true}).click()} const headers=async()=>({Origin:base,'X-CSRF-Token':(await(await page.request.get(base+'/api/realtime/capabilities')).json()).csrfToken}) try{ await start();await login() const agentResponse=await page.request.post(base+'/api/app/agents',{headers:await headers(),data:{name:'竹儿测试',profile:'default'}});expect(agentResponse.ok()).toBe(true) const agentId=(await agentResponse.json()).agent.id await settings();const panel=page.locator('.credential-vault') await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByLabel('确认主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'创建密码库',exact:true}).click() await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click();await panel.getByRole('button',{name:'添加凭据',exact:true}).click() await panel.getByRole('combobox',{name:/^用途/}).selectOption('ssh') await panel.getByLabel('名称',{exact:true}).fill('TrueNAS 流程测试');await panel.getByLabel('用户名',{exact:true}).fill(first.username);await panel.getByLabel('SSH 主机',{exact:true}).fill('127.0.0.1');await panel.getByLabel(/^SSH 登录密码/).fill(first.secret) await panel.getByText('高级连接设置(可选)',{exact:true}).click();await panel.getByLabel('端口',{exact:true}).fill(String(first.target.port)) await panel.getByRole('combobox',{name:/^授权给 Bot/}).selectOption(agentId) await panel.getByRole('button',{name:'保存并授权 Bot',exact:true}).click();await expect(panel.getByText('TrueNAS 流程测试 → 竹儿测试 · 已持续授权')).toBeVisible() expect(first.state.authMethods).toEqual([]) const state=await(await page.request.get(base+'/api/app/vault')).json(),credentialRef=state.entries[0].id const use=async(discoverOnly=false,command='uname -s')=>{const response=await page.request.post(base+'/__test/vault-use',{headers:await headers(),data:{agentId,credentialRef,discoverOnly,...(!discoverOnly?{command}:{})}});expect(response.ok(),await response.text()).toBe(true);return response.json()} await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click() expect((await use()).result).toEqual({status:'complete',operation:'ssh.exec',exitCode:0,stdout:'Linux\n',stderr:'',truncated:false});expect(first.state.commands).toEqual(['uname -s']) await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click() await panel.locator('li').filter({has:page.locator('strong', {hasText:'TrueNAS 流程测试'})}).getByRole('button',{name:'编辑',exact:true}).click() await expect(panel.getByText('已持续授权:竹儿测试。修改后继续可用,无需重新授权。')).toBeVisible() await panel.getByText('高级连接设置(可选)',{exact:true}).click();await panel.getByLabel('端口',{exact:true}).fill(String(second.target.port));await panel.getByRole('button',{name:'加密保存',exact:true}).click() await page.getByRole('button',{name:'关闭我的设置',exact:true}).filter({visible:true}).first().click();await expect(panel).toHaveCount(0);await expect.poll(async()=>(await(await page.request.get(base+'/api/app/vault')).json()).unlocked).toBe(false) const edited=await use(false,'df -h / | head -n 2');expect(edited.result.stdout).toContain('40%');expect(edited.refs.references[0]).toMatchObject({authorized:true,allowedTarget:{port:second.target.port}});expect(edited.result.exitCode).toBe(0) expect(first.state.commands).toHaveLength(1);expect(second.state.commands).toHaveLength(1) await stop();await start();await login() const restarted=await(await page.request.get(base+'/api/app/vault')).json();expect(restarted).toMatchObject({unlocked:false,botGrants:[{credentialRef,agentId}]}) expect((await use()).result).toEqual({status:'complete',operation:'ssh.exec',exitCode:0,stdout:'Linux\n',stderr:'',truncated:false});expect(second.state.commands).toHaveLength(2) await settings();await expect(panel.getByText('TrueNAS 流程测试 → 竹儿测试 · 已持续授权')).toBeVisible() await page.setViewportSize({width:375,height:812});expect(await panel.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true) mkdirSync('test-results/credential-vault-persistent',{recursive:true});await page.screenshot({path:'test-results/credential-vault-persistent/mobile-after-restart.png'}) await panel.getByLabel('主密码',{exact:true}).fill(master);await panel.getByRole('button',{name:'手动解锁 5 分钟',exact:true}).click();await panel.getByRole('button',{name:'撤销 Bot 授权',exact:true}).click();await panel.getByRole('button',{name:'锁定管理界面',exact:true}).click() expect(await use(true)).toMatchObject({references:[],reason:'bot_not_authorized'});expect(second.state.commands).toHaveLength(2) }finally{await stop();await first.close();await second.close();rmSync(home,{recursive:true,force:true})} })