import { expect, test } from '@playwright/test'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; const fixture = JSON.parse( readFileSync(resolve('web_platform/src/rl/fixtures/multiRingDeployment.json'), 'utf8'), ); // Original Go2 collision/inertia model, visual meshes omitted to keep the smoke fixture lightweight. const go2 = readFileSync( resolve('training_server/rl/src/assets/robots/unitree_go2/xmls/go2.xml'), 'utf8', ) .replace(/]*\/>/g, '') .replace(/]*\bmesh="[^"]*"[^>]*\/>/g, ''); let model = readFileSync( process.env.GO2_MULTI_SMOKE_POLICY ?? resolve('web_platform/fixtures/obstacle/multi-zero-action.onnx'), ); test('训练作业一键导入:真实WASM地图+97维ORT+PiP+射线开关', async ({ page }) => { await page.addInitScript(() => { localStorage.setItem('mujoco-local-training-job-id', 'a'.repeat(32)); sessionStorage.setItem('mujoco-local-training-token', 'test'); }); const job = { id: 'a'.repeat(32), taskId: 'Unitree-Go2-ObstacleAvoidance', state: 'succeeded', artifactReady: true, progress: 1, iteration: 1, maxIterations: 1, logs: [ 'Learning iteration 0 / 1', 'Mean value loss: 0.9', 'Mean surrogate loss: -0.1', 'Mean entropy loss: -1', 'Mean reward: 2', 'Mean episode length: 20', 'Learning iteration 1 / 1', 'Mean value loss: 0.5', 'Mean surrogate loss: -0.2', 'Mean entropy loss: -0.8', 'Mean reward: 3', 'Mean episode length: 30', ], message: '测试专用零动作策略', deployment: fixture, }; await page.route('http://127.0.0.1:8765/**', (route) => { const url = route.request().url(); if (url.endsWith('/policy.onnx')) return route.fulfill({ contentType: 'application/octet-stream', body: model }); if (url.endsWith('/health')) return route.fulfill({ json: { ready: true, trainerRoot: '/test', tasks: [job.taskId], activeJobId: job.id }, }); if (url.includes('/presets')) return route.fulfill({ json: { presets: [] } }); return route.fulfill({ json: job }); }); await page.goto('/'); await page .locator('input[type="file"]') .first() .setInputFiles({ name: 'go2.xml', mimeType: 'text/xml', buffer: Buffer.from(go2) }); await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 30_000 }); await page.getByRole('tab', { name: '控制台' }).click(); const tools = page.getByRole('tabpanel', { name: '控制台' }); await tools.getByRole('button', { name: /强化学习任务/ }).click(); await tools.getByRole('button', { name: '连接', exact: true }).click(); await expect(tools.getByRole('button', { name: '导入策略' })).toBeEnabled(); await tools.getByRole('button', { name: '导入策略' }).click(); await expect(page.getByText('训练配套物理地图', { exact: false })).toBeVisible({ timeout: 30_000, }); await expect(page.getByLabel('摄像头画面', { exact: true })).toBeVisible(); const rays = page.getByRole('checkbox', { name: '显示避障射线' }); await expect(rays).toBeChecked(); await rays.uncheck(); await rays.check(); const section = tools.getByRole('button', { name: /ONNX 策略运行/ }); if ((await section.getAttribute('aria-expanded')) === 'false') await section.click(); await expect(tools.getByText('97 / 12')).toBeVisible(); await expect(tools.getByText('Go2 前视射线避障导航', { exact: true })).toBeVisible(); await tools.getByText('推理详情', { exact: true }).click(); const count = tools.getByText('推理次数', { exact: true }).locator('..'); await expect .poll(async () => Number((await count.textContent())?.replace(/\D/g, ''))) .toBeGreaterThan(0); const pause = page.getByRole('button', { name: '⏸ 暂停' }); if (await pause.isVisible()) await pause.click(); const stop = tools.getByRole('button', { name: '停止', exact: true }); if (await stop.isVisible()) await stop.click(); await section.evaluate((element) => element.scrollIntoView({ block: 'start' })); await page.screenshot({ path: test.info().outputPath('onnx-policy.png') }); const targetRow = tools.getByText('当前目标 (X, Y) m', { exact: true }).locator('..'); const initialTarget = await targetRow.textContent(); const selection = () => page.locator('[aria-selected="true"]').allTextContents(); const beforeSelection = await selection(); await tools.getByRole('button', { name: '设定目标', exact: true }).click(); await page.keyboard.press('Escape'); await expect(tools.getByRole('button', { name: '设定目标', exact: true })).toHaveAttribute( 'aria-pressed', 'false', ); await expect(targetRow).toHaveText(initialTarget!); await tools.getByRole('button', { name: '设定目标', exact: true }).click(); const canvas = page.locator('.viewport-shell canvas').first(); const bounds = (await canvas.boundingBox())!; await canvas.click({ position: { x: bounds.width * 0.5, y: bounds.height * 0.55 } }); await expect(targetRow).not.toHaveText(initialTarget!); await expect(tools.getByRole('button', { name: '设定目标', exact: true })).toHaveAttribute( 'aria-pressed', 'false', ); expect(await selection()).toEqual(beforeSelection); await expect(tools.getByRole('button', { name: '启用', exact: true })).toBeVisible(); await tools.getByRole('button', { name: '复位目标点' }).click(); await expect(targetRow).toHaveText(initialTarget!); await tools.getByRole('button', { name: '导入策略' }).click(); await expect(tools.getByRole('alert')).toHaveCount(0); // Switch real valid graph/metadata pairs in both directions, without relaxing transactions. const multiModel = model; job.deployment = JSON.parse( readFileSync(resolve('web_platform/src/rl/fixtures/obstacleDeployment.json'), 'utf8'), ); model = readFileSync(resolve('web_platform/fixtures/obstacle/zero-action.onnx')); await tools.getByRole('button', { name: '连接', exact: true }).click(); await tools.getByRole('button', { name: '导入策略' }).click(); await expect(tools.getByText('81 / 12')).toBeVisible(); job.deployment = fixture; model = multiModel; await tools.getByRole('button', { name: '连接', exact: true }).click(); await tools.getByRole('button', { name: '导入策略' }).click(); await expect(tools.getByText('97 / 12')).toBeVisible(); const pauseAgain = page.getByRole('button', { name: '⏸ 暂停' }); if (await pauseAgain.isVisible()) await pauseAgain.click(); const stopAgain = tools.getByRole('button', { name: '停止', exact: true }); if (await stopAgain.isVisible()) await stopAgain.click(); // A real 81-feature ONNX graph with valid 97 metadata must fail transactionally. model = readFileSync(resolve('web_platform/fixtures/obstacle/multi-wrong-graph.onnx')); await tools.getByRole('button', { name: '导入策略' }).click(); await expect(tools.getByRole('alert')).toContainText('维度'); await expect(tools.getByText('97 / 12')).toBeVisible(); await page.screenshot({ path: test.info().outputPath('onnx-contract-error.png') }); await expect(targetRow).toHaveText(initialTarget!); await tools.getByRole('button', { name: '设定目标', exact: true }).click(); await tools.getByRole('button', { name: '卸载', exact: true }).click(); await expect(targetRow).toHaveCount(0); await expect(tools.locator('.uplot')).toHaveCount(0); await tools.getByRole('button', { name: /训练指标趋势/ }).click(); await expect(tools.locator('.uplot canvas')).toHaveCount(1); await tools.getByRole('tab', { name: '综合', exact: true }).click(); await expect(tools.locator('.uplot canvas')).toHaveCount(5); await page.screenshot({ path: test.info().outputPath('training-metrics.png') }); await tools.getByRole('button', { name: /训练指标趋势/ }).click(); await expect(tools.locator('.uplot')).toHaveCount(0); });