Files
Mujoco_WASM/web_platform/e2e/lekiwi.variants.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

138 lines
6.4 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import type {} from '../physics/runner';
import type { ProjectManifest } from '../src/project/types';
const assetBase = `/@fs${resolve(process.cwd(), 'build/lekiwi')}`;
const hash = (text: string) => createHash('sha256').update(text).digest('hex');
test('等价 URDF 变体:真实 WASM 控制、输入溯源及 MJCF 导出重载', async ({ page }, info) => {
const original = await readFile('robot_profiles/lekiwi-reference.urdf', 'utf8');
const variant = original
.replace('name="LeKiwi"', 'name="LeKiwi 换色变体"')
.replace('</visual>', '<material name="red"><color rgba="1 0 0 1"/></material></visual>');
await page.route('**/URDF/LeKiwi.urdf', (route) =>
route.fulfill({ body: variant, contentType: 'application/xml' }),
);
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
const boot = await page.evaluate((path) => window.lekiwiPhysics.boot(path), assetBase);
expect(boot.nu).toBe(9);
expect(boot.njnt).toBe(46);
const stable = await page.evaluate(() => window.lekiwiPhysics.step(3));
expect(stable.finite).toBe(true);
const drive = await page.evaluate(() => window.lekiwiPhysics.drive(0.1, 0, 0, 2));
expect(drive.position[0] - stable.position[0]).toBeGreaterThan(0.15);
const xml = await page.evaluate(() => window.lekiwiPhysics.xml());
expect(xml).toContain(`name="platform_robot_source_sha256" data="${hash(original)}"`);
expect(xml).toContain(`name="platform_robot_input_sha256" data="${hash(variant)}"`);
const reloaded = await page.evaluate(() => window.lekiwiPhysics.reimport());
expect(reloaded.robot?.profileId).toBe('lekiwi-v1');
expect(reloaded.nu).toBe(9);
expect(await page.evaluate(() => window.lekiwiPhysics.xml())).toContain(hash(variant));
await writeFile(info.outputPath('variant.xml'), xml);
await writeFile(
info.outputPath('variant-control.json'),
JSON.stringify({ boot, stable, drive, reloaded }, null, 2),
);
await page.evaluate(() => window.lekiwiPhysics.dispose());
});
test('同名底盘 STL 被改动也拒绝旧 profile,不只校验机械臂网格', async ({ page }) => {
await page.route('**/URDF/meshes/base_plate_layer1-v5.stl', (route) =>
route.fulfill({ body: 'changed mesh bytes' }),
);
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
await expect(page.evaluate((path) => window.lekiwiPhysics.boot(path), assetBase)).rejects.toThrow(
/SHA-256 不匹配.*base_plate_layer1/,
);
await page.evaluate(() => window.lekiwiPhysics.dispose());
});
// Optional local evidence: assets stay outside the repository. Setting the env
// explicitly makes missing/invalid samples fail, rather than silently skipping.
const localDirectory = process.env.LEKIWI_VARIANT_DIR;
const localSamples = localDirectory
? readdirSync(localDirectory)
.filter((file) => file.endsWith('.zip'))
.sort()
: [];
if (localDirectory && !localSamples.length) throw new Error('LEKIWI_VARIANT_DIR 中没有 ZIP 样本');
for (const name of localSamples) {
test(`用户变体包诊断(不适配旧控制):${name}`, async ({ page }, info) => {
await page.route('**/__local_variant.zip', (route) =>
route.fulfill({ path: resolve(localDirectory!, name), contentType: 'application/zip' }),
);
await page.goto('/physics/runner.html');
const result = await page.evaluate(async () => {
const importerPath = '/src/project/importer.ts';
const profilesPath = '/src/project/robotProfiles/index.ts';
const physicsPath = '/src/simulation/PhysicsAdapter.ts';
const registryPath = '/src/robot/registry.ts';
const { importBrowserFiles, prepareProjectForMujoco } = await import(
/* @vite-ignore */ importerPath
);
const { prepareRobotProject } = await import(/* @vite-ignore */ profilesPath);
const { MainThreadPhysicsAdapter } = await import(/* @vite-ignore */ physicsPath);
const { sha256 } = await import(/* @vite-ignore */ registryPath);
const bytes = await (await fetch('/__local_variant.zip')).arrayBuffer();
const manifest: ProjectManifest = await importBrowserFiles([
new File([bytes], 'variant.zip'),
]);
const entry = manifest.selectedEntry!;
const data = manifest.files.find((file) => file.path === entry)!.data;
const sourceHash = await sha256(data);
const doc = new DOMParser().parseFromString(
new TextDecoder().decode(data),
'application/xml',
);
const movable = Array.from(doc.querySelectorAll('robot > joint')).filter(
(j) => j.getAttribute('type') !== 'fixed',
);
let profileError = '';
try {
await prepareRobotProject(manifest, entry, 'lekiwi-v1');
} catch (error) {
profileError = String(error);
}
const prepared = await prepareProjectForMujoco(manifest, entry);
const fixedText = new TextDecoder().decode(
prepared.manifest.files.find((file: { path: string }) => file.path === entry).data,
);
const adapter = new MainThreadPhysicsAdapter();
let genericCompileError = '';
try {
await adapter.load(manifest, entry, { urdfMode: 'mjcf', baseMode: 'floating' });
} catch (error) {
genericCompileError = String(error);
} finally {
adapter.dispose();
}
return {
entry,
sourceHash,
profileError,
genericCompileError,
links: doc.querySelectorAll('robot > link').length,
movable: movable.map((j) => j.getAttribute('name')),
warnings: prepared.warnings,
unresolvedPackageUri: fixedText.includes('package://'),
sourceUnmodified: (await sha256(data)) === sourceHash,
};
});
expect(result.links).toBe(35);
expect(result.movable).toHaveLength(10);
expect(result.profileError).toContain('需要 9 个活动关节,实际 10 个');
expect(result.profileError).toContain('Link7');
expect(result.unresolvedPackageUri).toBe(false);
expect(result.sourceUnmodified).toBe(true);
// This is an import repair, not an unreviewed wheel-mesh simplification.
expect(result.genericCompileError).toMatch(/STL|faces/i);
await writeFile(info.outputPath('local-variant-audit.json'), JSON.stringify(result, null, 2));
});
}