feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
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
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 验收仍待用户凭据。
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user