159 lines
7.8 KiB
TypeScript
159 lines
7.8 KiB
TypeScript
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(/<mesh\b[^>]*\/>/g, '')
|
|
.replace(/<geom\b[^>]*\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);
|
|
});
|