Files
Mujoco_WASM/web_platform/e2e/mobile.spec.ts
T
chenlin f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
2026-09-24 09:57:41 +08:00

182 lines
8.0 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { existsSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import type {} from '../physics/mobile';
const original = resolve('build/lekiwi/lekiwi-v1.zip');
const bundle =
process.env.MOBILE_BUNDLE_ZIP ??
resolve('../Reference_Projects/LeKiwi/New_urdf/robot_urdf_bundle (1).zip');
async function exportRollout(page: Page, zipName: string, traceName: string) {
await page.evaluate(() => window.mobilePhysics.reset());
await mkdir('build/mobile-validation', { recursive: true });
const downloadPromise = page.waitForEvent('download');
await page.evaluate(() => window.mobilePhysics.download());
await (await downloadPromise).saveAs(`build/mobile-validation/${zipName}`);
const actions = Array.from({ length: 12 }, (_, step) => {
const a = new Array<number>(12).fill(0);
a[0] = 0.1;
a[3] = 0.03 * Math.sin(step);
a[11] = 1;
return a;
});
const rollout = [];
for (const action of actions)
rollout.push(await page.evaluate((a) => window.mobilePhysics.step(1, a), action));
await writeFile(
`build/mobile-validation/${traceName}`,
JSON.stringify({ actions, rollout }, null, 2),
);
}
test('真实 WASM:两种机器人、控制、拖动、失败回滚、重复切换与训练包导出', async ({ page }) => {
test.skip(!existsSync(original) || !existsSync(bundle), '需要本地两个机器人 ZIP');
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.route('**/original.zip', (route) => route.fulfill({ path: original }));
await page.route('**/bundle.zip', (route) => route.fulfill({ path: bundle }));
await page.goto('/physics/mobile.html');
await page.waitForFunction(() => Boolean(window.mobilePhysics));
const traces = [];
for (const id of [
'lekiwi-v1',
'lekiwi-bundle',
'lekiwi-v1',
'lekiwi-bundle',
'lekiwi-v1',
'lekiwi-bundle',
'lekiwi-v1',
'lekiwi-bundle',
// Complete CAD meshes increase allocator/cache warm-up; exercise extra
// cycles without raising the peak-memory bound or weakening final stability.
'lekiwi-v1',
'lekiwi-bundle',
'lekiwi-v1',
'lekiwi-bundle',
]) {
const initial = await page.evaluate(
(id) => window.mobilePhysics.boot(id === 'lekiwi-v1' ? '/original.zip' : '/bundle.zip', id),
id,
);
console.log('mobile heap', id, initial.heap / 1048576);
expect(initial.heap).toBeLessThan(1600 * 1048576);
expect(initial.observation).toHaveLength(92);
expect(initial.observation.every(Number.isFinite)).toBe(true);
expect(initial.ctrl).toHaveLength(9);
expect(initial.retiredModelDeleted).toBe(true);
expect(initial.retiredDataDeleted).toBe(true);
const stable = await page.evaluate(() => window.mobilePhysics.step(10));
expect(stable.time).toBeCloseTo(0.2, 8);
expect(stable.qpos.every(Number.isFinite)).toBe(true);
const action = new Array<number>(12).fill(0);
action[0] = 0.3;
action[3] = 0.2;
action[11] = 1;
const moved = await page.evaluate((a) => window.mobilePhysics.step(10, a, true), action);
expect(moved.ctrl.some((v, i) => Math.abs(v - initial.ctrl[i]) > 0.01)).toBe(true);
expect(moved.qpos.some((v, i) => Math.abs(v - stable.qpos[i]) > 0.001)).toBe(true);
action[0] = 0;
action[11] = -1;
const closed = await page.evaluate((a) => window.mobilePhysics.step(80, a, true), action);
expect(closed.state[29]).toBeLessThan(initial.state[29] - 0.2);
expect(closed.qpos.every(Number.isFinite)).toBe(true);
expect(await page.evaluate(() => window.mobilePhysics.workspaces())).toHaveLength(1);
const edited = await page.evaluate(() => window.mobilePhysics.move('goal', [0.4, -0.2, 0.019]));
expect(edited.state.slice(47, 50)).toEqual([0.4, -0.2, 0.019]);
expect(edited.stage).toBe('reach');
traces.push({ initial, stable, moved, closed, edited });
if (traces.length === 1)
await exportRollout(page, 'lekiwi-training.zip', 'lekiwi-rollout.json');
}
const previous = await page.evaluate(() => window.mobilePhysics.state());
const failure = await page.evaluate(async () => {
try {
await window.mobilePhysics.boot('/original.zip', 'lekiwi-bundle');
return '';
} catch (e) {
return String(e);
}
});
expect(failure).toContain('Link1');
expect((await page.evaluate(() => window.mobilePhysics.state())).robotId).toBe(previous.robotId);
expect(await page.evaluate(() => window.mobilePhysics.workspaces())).toHaveLength(1);
const compileFailure = await page.evaluate(async () => {
try {
await window.mobilePhysics.invalidCandidate('/bundle.zip');
return '';
} catch (e) {
return String(e);
}
});
expect(compileFailure).toContain('missing_actuator');
expect(await page.evaluate(() => window.mobilePhysics.workspaces())).toHaveLength(1);
expect(
(await page.evaluate(() => window.mobilePhysics.step(1))).observation.every(Number.isFinite),
).toBe(true);
await writeFile(
'build/mobile-validation/heap-warmup.json',
JSON.stringify(
traces.map((t) => ({ robot: t.initial.robotId, heap: t.initial.heap })),
null,
2,
),
);
expect(traces[11].initial.heap).toBe(traces[9].initial.heap);
await exportRollout(page, 'bundle-training.zip', 'rollout.json');
await writeFile('build/mobile-validation/traces.json', JSON.stringify(traces, null, 2));
await page.evaluate(() => {
window.mobilePhysics.reset();
window.mobilePhysics.show();
});
await page.waitForFunction(() => document.querySelector('canvas')!.width > 500);
// Use actual camera projection, not hard-coded screenshot pixel coordinates.
for (const entity of ['object', 'goal'] as const) {
const before = await page.evaluate(() => window.mobilePhysics.state());
const p = await page.evaluate((entity) => window.mobilePhysics.screen(entity), entity);
await page.keyboard.down('Shift');
await page.mouse.move(p.x, p.y);
await page.mouse.down();
await page.mouse.move(p.x + 45, p.y + 12, { steps: 5 });
await page.mouse.up();
await page.keyboard.up('Shift');
const after = await page.evaluate(() => window.mobilePhysics.state());
const offset = entity === 'object' ? 37 : 47;
expect(
Math.hypot(
after.state[offset] - before.state[offset],
after.state[offset + 1] - before.state[offset + 1],
),
).toBeGreaterThan(0.01);
}
expect(errors).toEqual([]);
await page.evaluate(() => window.mobilePhysics.dispose());
});
// 独立产品页已移除;导入/控制/暂停/重置/状态 UI 在主工作台用例验收:
// lekiwi.agent.workspace.spec.ts。这里保留两模型物理、训练快照及 ORT 夹具。
test('真实 ORT WASM:导出的 smoke 权重锁步执行(不代表训练成功)', async ({ page }) => {
const model = resolve('build/mobile-validation/smoke.onnx');
test.skip(!existsSync(model) || !existsSync(bundle), '先按文档导出 smoke.onnx');
const metadata = JSON.parse(await readFile('build/mobile-validation/smoke.json', 'utf8'));
await page.route('**/bundle.zip', (route) => route.fulfill({ path: bundle }));
await page.route('**/smoke.onnx', (route) => route.fulfill({ path: model }));
await page.goto('/physics/mobile.html');
await page.waitForFunction(() => Boolean(window.mobilePhysics));
await page.evaluate(() => window.mobilePhysics.boot('/bundle.zip', 'lekiwi-bundle'));
await page.evaluate((metadata) => window.mobilePhysics.policy('/smoke.onnx', metadata), metadata);
await expect
.poll(
async () => (await page.evaluate(() => window.mobilePhysics.tick(performance.now()))).time,
{ timeout: 30_000, intervals: [20] },
)
.toBeGreaterThanOrEqual(0.1);
const status = await page.evaluate(() => window.mobilePhysics.tick(performance.now()));
expect(status.metrics?.error).toBe('');
expect(status.metrics!.inferenceCount).toBeGreaterThanOrEqual(5);
await writeFile('build/mobile-validation/onnx-metrics.json', JSON.stringify(status, null, 2));
await page.evaluate(() => window.mobilePhysics.dispose());
});