import { expect, test } from '@playwright/test'; import { resolve } from 'node:path'; import { existsSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; const prefix = '/api/decision/v1'; test('网站生产构建:精简设置、同源 Cookie/CSRF、双浏览器隔离与假 HTTP 上游', async ({ page, browser, }, info) => { const errors: string[] = []; page.on('pageerror', (e) => errors.push(e.message)); await page.goto('/'); await expect(page).toHaveTitle(/CADWorld Sim/); await page .locator('input[type=file]') .first() .setInputFiles(resolve('web_platform/fixtures/python_controller/model.xml')); await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 60000 }); await page.getByRole('tab', { name: '控制台', exact: true }).click(); await expect(page.getByRole('button', { name: '强化学习任务', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '开源项目 / 外部控制', exact: true })).toHaveCount( 0, ); await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click(); await page.getByText('模型设置 · API_KEY / ChatGPT 订阅', { exact: true }).click(); await expect(page.getByRole('button', { name: '保存设置' })).toBeEnabled(); await expect(page.getByLabel('本机服务令牌')).toHaveCount(0); await page.getByLabel('LLM API_KEY', { exact: true }).fill('e2e-llm-fixture-not-real'); await page.getByLabel('Jev API_KEY(OpenRouter)').fill('e2e-jev-fixture-not-real'); await page.getByRole('button', { name: '保存设置', exact: true }).click(); await expect(page.getByRole('status').filter({ hasText: '设置已保存' })).toBeVisible(); await expect(page.getByLabel('LLM API_KEY', { exact: true })).toHaveValue(''); await page.getByText('连接测试与凭据管理', { exact: true }).click(); await page.getByRole('button', { name: '测试 LLM(可能计费)', exact: true }).click(); await expect(page.getByText(/LLM 连接测试通过/)).toBeVisible(); const other = await browser.newContext(); try { const second = await other.newPage(); await second.goto('/'); const fresh = await second.evaluate(async (prefix) => { const response = await fetch(prefix + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); return response.json(); }, prefix); expect(fresh.ready).toBe(false); const state = await page.evaluate( async (prefix) => (await fetch(prefix + '/status')).json(), prefix, ); expect(state.ready).toBe(true); const forbidden = await page.evaluate( async (prefix) => ( await fetch(prefix + '/configuration', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}', }) ).status, prefix, ); expect(forbidden).toBe(403); expect(await page.evaluate(() => document.cookie)).not.toContain('cadworld'); expect( await page.evaluate(() => JSON.stringify({ ...localStorage, ...sessionStorage })), ).not.toContain('e2e-llm'); } finally { await other.close(); } await page.getByRole('button', { name: 'ChatGPT 订阅', exact: true }).click(); await expect(page.getByRole('button', { name: '登录 ChatGPT 订阅', exact: true })).toBeVisible(); await page.screenshot({ path: info.outputPath('website-settings.png') }); expect(errors).toEqual([]); }); test('网站 A 模型:真实 WASM、显式 mock 抓放和证据导出(无远端模型调用)', async ({ page, }, info) => { test.skip(!existsSync('build/lekiwi/lekiwi-v1.zip'), '需要本地 A 模型,不自动公开或下载资产'); let calls = 0; page.on('request', (r) => { if (/\/api\/decision\/v1\/(plan|decide)$/.test(r.url())) calls++; }); await page.goto('/'); await page .locator('input[type=file]') .first() .setInputFiles(resolve('build/lekiwi/lekiwi-v1.zip')); const dialog = page.getByRole('dialog', { name: '配置 URDF 仿真组件' }); await expect(dialog).toBeVisible({ timeout: 30000 }); await dialog.getByLabel('URDF 机器人 profile').selectOption('lekiwi-v1'); await dialog.getByRole('button', { name: '转换并加载' }).click(); await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 60000 }); await page.getByRole('tab', { name: '控制台', exact: true }).click(); await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click(); const panel = page.getByLabel('LeKiwi 智能抓放任务', { exact: true }); await panel.getByRole('button', { name: '开始任务(mock)', exact: true }).click(); await expect(panel.getByTestId('agent-task-state')).toContainText('物理验收成功', { timeout: 180000, }); await panel.getByText('高级操作与证据导出', { exact: true }).click(); const downloaded = page.waitForEvent('download'); await panel.getByRole('button', { name: '导出任务证据', exact: true }).click(); const evidence = JSON.parse(await readFile((await (await downloaded).path())!, 'utf8')); expect(evidence.mode).toBe('mock'); expect(evidence.succeeded).toBe(true); expect(evidence.transported).toBeGreaterThanOrEqual(0.5); expect(evidence.sample.onGoalSupport).toBe(true); expect(calls).toBe(0); await writeFile( info.outputPath('website-physical-evidence.json'), JSON.stringify(evidence, null, 2), ); await page.screenshot({ path: info.outputPath('website-grasp.png') }); });