import { expect, test } from '@playwright/test'; import { resolve } from 'node:path'; import { readFile, writeFile } from 'node:fs/promises'; test('主工作台:A 导入、mock 单技能暂停/继续、物理成功及证据导出', async ({ page }, info) => { const errors: string[] = []; let modelRequests = 0; page.on('pageerror', (e) => errors.push(e.message)); page.on('request', (r) => { if (/\/api\/decision\/v1\/(plan|decide)$/.test(r.url())) modelRequests++; }); 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 expect(panel).toContainText('MOCK(不调用模型 API)'); await panel.getByRole('button', { name: '加载 A 任务场景', exact: true }).click(); await expect(panel).toContainText('A 抓放场景已加载并暂停', { timeout: 60000 }); await panel.getByRole('button', { name: '单技能步进', exact: true }).click(); const status = panel.getByTestId('agent-task-state'); await expect(status).toContainText('任务已暂停', { timeout: 60000 }); await expect(status).toContainText('当前技能:open'); await expect(status).toContainText('Jev 1/60'); await panel.getByRole('button', { name: '继续任务', exact: true }).click(); await expect(status).toContainText('物理验收成功', { timeout: 180000 }); await expect(status).toContainText('Jev 11/60'); const download = page.waitForEvent('download'); await panel.getByRole('button', { name: '导出任务证据', exact: true }).click(); const downloaded = await download; const evidence = JSON.parse(await readFile((await downloaded.path())!, 'utf8')); await writeFile( info.outputPath('workbench-physical-evidence.json'), JSON.stringify(evidence, null, 2), ); expect(evidence.mode).toBe('mock'); expect(evidence.succeeded).toBe(true); expect(evidence.transported).toBeGreaterThanOrEqual(0.5); expect(evidence.sample.onGoalSupport).toBe(true); expect(modelRequests).toBe(0); expect(errors).toEqual([]); await page.screenshot({ path: info.outputPath('agent-workbench.png') }); await panel.getByRole('button', { name: '重置任务场景', exact: true }).click(); await expect(panel.getByTestId('agent-task-state')).toHaveCount(0, { timeout: 60000 }); await panel.getByLabel('目标 Y', { exact: true }).fill('0.617'); await panel.getByRole('button', { name: '单技能步进', exact: true }).click(); await expect(status).toContainText('任务已暂停', { timeout: 60000 }); const changedDownload = page.waitForEvent('download'); await panel.getByRole('button', { name: '导出任务证据', exact: true }).click(); const changed = JSON.parse(await readFile((await (await changedDownload).path())!, 'utf8')); expect(changed.goal).toEqual([0.257, 0.617, 0.128]); expect(changed.runId).not.toBe(evidence.runId); expect(changed.sample.time).toBeLessThan(2.01); expect(changed.sample.object[1]).toBeLessThan(0.02); // New source episode, not the old placed cube. await writeFile(info.outputPath('edited-goal-episode.json'), JSON.stringify(changed, null, 2)); await panel.getByLabel('目标 Y', { exact: true }).fill('0.615'); await expect(status).toContainText('已取消'); await expect(status).toContainText('目标编辑使旧计划失效'); expect(modelRequests).toBe(0); expect(errors).toEqual([]); await expect(page.getByRole('button', { name: '强化学习任务', exact: true })).toBeVisible(); });