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
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
182 lines
8.0 KiB
TypeScript
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());
|
|
});
|