Files
Mujoco_WASM/web_platform/e2e/pretrainedUpload.spec.ts
chenlin 13e35be98b
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
feat(web-platform): release V0.9.3 全模块界面重构
2026-09-08 17:57:57 +08:00

180 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from '@playwright/test';
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
const realRoot = process.env.GO2_UPLOAD_REAL_DIR;
const token = 'local-upload-browser-test-token';
async function unusedPort() {
const socket = createServer();
await new Promise<void>((done) => socket.listen(0, '127.0.0.1', done));
const address = socket.address();
const port = typeof address === 'object' && address ? address.port : 0;
await new Promise<void>((done) => socket.close(() => done()));
return port;
}
for (const panel of ['ordinary', 'tuning'] as const) {
for (const format of ['pt', 'onnx'] as const) {
test(`${panel}真实单${format}文件:默认服务空目录上传并选择内容ID`, async ({
page,
}, testInfo) => {
test.skip(!realRoot, '设置GO2_UPLOAD_REAL_DIRVite dev提供真实组件,启动独立默认训练服务');
const root = mkdtempSync(join(tmpdir(), `go2-browser-upload-${panel}-${format}-`));
const port = await unusedPort();
const endpoint = `http://127.0.0.1:${port}`;
const backend = spawn(
resolve('.venv/bin/python'),
[
'-u',
'training_server/server.py',
'--port',
String(port),
'--token',
token,
'--tuning-data-root',
root,
],
{
cwd: process.cwd(),
env: { ...process.env, DEEPSEEK_API_KEY: '', WANDB_MODE: 'disabled' },
},
);
let log = '';
backend.stdout.on('data', (data: Buffer) => {
log += data.toString();
});
backend.stderr.on('data', (data: Buffer) => {
log += data.toString();
});
try {
await expect
.poll(
async () => {
try {
return (
await fetch(`${endpoint}/api/training/health`, {
headers: { Authorization: `Bearer ${token}` },
})
).status;
} catch {
return 0;
}
},
{ timeout: 30_000 },
)
.toBe(200);
await page.goto(process.env.GO2_PRETRAINED_DEV_URL ?? 'http://127.0.0.1:4174');
// Real production panel components, isolated from unrelated heavy scene import.
await page.evaluate(async (panel) => {
localStorage.clear();
sessionStorage.clear();
const entry = await (await fetch('/src/main.tsx')).text();
const reactPath = entry.match(/from "([^"]+\/react\.js[^"]*)"/)![1];
const domPath = entry.match(/from "([^"]+\/react-dom_client\.js[^"]*)"/)![1];
const { createElement } = (await import(reactPath)).default;
const { createRoot } = (await import(domPath)).default;
const host = document.createElement('div');
host.id = 'upload-browser-panel';
host.style.cssText =
'position:fixed;inset:0;z-index:10000;background:#111;overflow:auto;padding:20px';
document.body.append(host);
if (panel === 'ordinary') {
const path = '/src/training/LocalTrainingPanel.tsx';
const { LocalTrainingPanel } = await import(path);
createRoot(host).render(createElement(LocalTrainingPanel, { onPolicyReady: () => {} }));
} else {
const path = '/src/tuning/TuningApp.tsx';
const { TuningApp } = await import(path);
createRoot(host).render(createElement(TuningApp));
}
}, panel);
const ui = page.locator('#upload-browser-panel');
await ui
.getByLabel(panel === 'ordinary' ? '本地训练服务地址' : '训练服务地址', { exact: true })
.fill(endpoint);
await ui
.getByLabel(panel === 'ordinary' ? '训练服务访问令牌' : '访问令牌(仅当前标签页)', {
exact: true,
})
.fill(token);
await ui
.getByRole('button', { name: panel === 'ordinary' ? /^连接$/ : '连接/刷新' })
.click();
await ui.getByText('上传基础策略', { exact: true }).click();
await expect(ui.getByLabel('确认Go2 legacy47模板')).toBeEnabled();
await expect(ui.getByLabel('基础策略', { exact: true })).toHaveValue('');
await ui.getByLabel('确认Go2 legacy47模板').check();
const path = join(realRoot!, format === 'pt' ? 'model_10000.pt' : 'policy.onnx');
const sha = createHash('sha256').update(readFileSync(path)).digest('hex');
const responsePromise = page.waitForResponse((response) =>
response.url().startsWith(`${endpoint}/api/training/pretrained-sources/upload?`),
);
await ui.getByLabel('选择基础策略文件').setInputFiles(path);
const response = await responsePromise;
expect(response.status()).toBe(201);
const catalog = await (
await fetch(`${endpoint}/api/training/health`, {
headers: { Authorization: `Bearer ${token}` },
})
).json();
const record = catalog.pretrainedSources[0];
await expect(ui.getByLabel('基础策略', { exact: true })).toHaveValue(record.id);
await ui.getByText('来源与校验详情', { exact: true }).click();
await expect(ui.getByText(/原文件 SHA256/)).toContainText(sha);
await expect(ui.getByText(/上传格式/)).toContainText(format);
await expect(ui.getByText(/已选择.*仅继承策略权重/)).toBeVisible();
if (format === 'onnx')
await expect(ui.getByText(/ONNX统计count合成/)).toContainText('1000000');
// Intercept only train dispatch; uploads and catalog are real authenticated HTTP.
// Never dispatch 4096-env training or an Agent/three-seed evaluation from this test.
let submitted: Record<string, unknown> | undefined;
await page.route(
`${endpoint}${panel === 'ordinary' ? '/api/training/jobs' : '/api/tuning/sessions'}`,
async (route) => {
if (route.request().method() !== 'POST') return route.continue();
submitted = route.request().postDataJSON() as Record<string, unknown>;
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: '浏览器验收只截获启动请求,未训练或调用Agent' }),
});
},
);
if (panel === 'tuning') await ui.getByLabel('Agent 失败时允许 Optuna fallback').check();
await ui
.getByRole('button', { name: panel === 'ordinary' ? '发起本地训练' : '启动自调参' })
.click();
await expect.poll(() => submitted?.pretrainedSourceId).toBe(record.id);
if (panel === 'tuning') expect(submitted?.mode).toBe('approval');
await expect(ui.getByLabel('基础策略', { exact: true })).toHaveValue(record.id);
const health = await (
await fetch(`${endpoint}/api/training/health`, {
headers: { Authorization: `Bearer ${token}` },
})
).json();
expect(health.pretrainedSources).toHaveLength(1);
expect(createHash('sha256').update(readFileSync(path)).digest('hex')).toBe(sha);
await testInfo.attach('upload-contract', {
body: JSON.stringify(
{ root, endpoint, record, submitted, sourceUnchanged: true },
null,
2,
),
contentType: 'application/json',
});
await page.screenshot({ path: testInfo.outputPath('upload.png') });
} finally {
backend.kill('SIGTERM');
await new Promise<void>((done) => {
if (backend.exitCode !== null) done();
else backend.once('exit', () => done());
});
writeFileSync(join(root, 'browser-server.log'), log);
}
});
}
}