Add full source parity validation workflow
This commit is contained in:
39
web/tests/e2e/ffmpeg-movie-probe-production.spec.ts
Normal file
39
web/tests/e2e/ffmpeg-movie-probe-production.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const fixture = fs.readFileSync(path.join(root, "tests/files/web/media/sequencer-probe.mp4"));
|
||||
const sourceSha256 = crypto.createHash("sha256").update(fixture).digest("hex");
|
||||
|
||||
test("WBV2-P1128-000 probes and decodes a movie through the local ffmpeg.wasm Worker", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async ({ bytes, sourceSha256 }) => {
|
||||
const [{ probeSequencerCodec, probeSequencerMovieCodecWithFallback }, cache] = await Promise.all([
|
||||
import("/src/sequencer/SequencerCodecProbe.ts"),
|
||||
import("/src/sequencer/SequencerMediaProxyCache.ts"),
|
||||
]);
|
||||
const source = Uint8Array.from(bytes).buffer;
|
||||
const request = { schemaVersion: 1 as const, stripType: "MOVIE" as const, mimeType: "video/mp4", byteLength: source.byteLength, sourceSha256 };
|
||||
const browser = await probeSequencerCodec(request, source);
|
||||
const capability = await probeSequencerMovieCodecWithFallback(request, source);
|
||||
const frame = await cache.generateInitialSequencerMovieProxyFrame(request, capability, { kind: "MOVIE_RGBA8_FRAME", width: 8, height: 8, colorSpace: "SRGB8", alphaMode: "STRAIGHT" }, source);
|
||||
const corrupt = new Uint8Array(source.slice(0));
|
||||
corrupt.fill(0, 0, Math.min(32, corrupt.length));
|
||||
const corruptHash = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", corrupt)), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
const corruptResult = await probeSequencerMovieCodecWithFallback({ ...request, sourceSha256: corruptHash }, corrupt.buffer);
|
||||
const cancellation = new AbortController();
|
||||
cancellation.abort();
|
||||
const cancelled = await probeSequencerMovieCodecWithFallback(request, source, cancellation.signal).then(() => false).catch((error) => error?.name === "AbortError");
|
||||
return { browser, capability, frameBytes: frame.data.byteLength, frameManifest: frame.manifest, corrupt: { status: corruptResult.status, reason: corruptResult.reason }, cancelled };
|
||||
}, { bytes: Array.from(fixture), sourceSha256 });
|
||||
|
||||
expect(result.browser.status).toBe("BLOCKED");
|
||||
expect(result.capability).toMatchObject({ status: "READY", backend: "FFMPEG_WASM", decoded: { width: 16, height: 16, durationMicros: 1_000_000 } });
|
||||
expect(result.frameBytes).toBe(8 * 8 * 4);
|
||||
expect(result.frameManifest.decodeCapability).toEqual(result.capability);
|
||||
expect(result.corrupt).toEqual({ status: "BLOCKED", reason: "DECODE_FAILED" });
|
||||
expect(result.cancelled).toBe(true);
|
||||
});
|
||||
@@ -80,7 +80,13 @@ test("M7-04 destroys isolated native open resources before reporting cancellatio
|
||||
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
|
||||
const previousStats = await page.getByTestId("scene-stats").textContent();
|
||||
|
||||
await page.getByTestId("blend-file-input").setInputFiles(largeBlend);
|
||||
const nativeCancellationBlend = Buffer.alloc(40 * 1024 * 1024);
|
||||
fs.readFileSync(largeBlend).copy(nativeCancellationBlend);
|
||||
await page.getByTestId("blend-file-input").setInputFiles({
|
||||
name: "native-cancellation.blend",
|
||||
mimeType: "application/octet-stream",
|
||||
buffer: nativeCancellationBlend,
|
||||
});
|
||||
const cancellation = await page.waitForFunction(() => {
|
||||
const progress = document.querySelector<HTMLElement>('[data-testid="open-progress"]');
|
||||
const stage = progress?.dataset.stage;
|
||||
|
||||
@@ -58,7 +58,7 @@ test("M7-16 completes the P0 project loop without pointer input and passes the a
|
||||
await expect(page.getByTestId("scene-stats")).toContainText("Objects 3");
|
||||
|
||||
await page.keyboard.press("F3");
|
||||
const search = page.getByRole("textbox", { name: "搜索操作" });
|
||||
const search = page.getByRole("textbox", { name: "搜索 Blender 操作" });
|
||||
await expectVisibleKeyboardFocus(search);
|
||||
await page.keyboard.insertText("Add Cube");
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend");
|
||||
@@ -19,8 +20,12 @@ test("keeps Main edit and save-reopen available during a Chromium network interr
|
||||
expect(download.suggestedFilename()).toBe("blender-web.blend");
|
||||
const savedPath = await download.path();
|
||||
expect(savedPath).not.toBeNull();
|
||||
await page.setInputFiles("[data-testid=blend-file-input]", savedPath!);
|
||||
await expect(page.getByTestId("engine-status")).toContainText("SceneIR r3 (4 objects)");
|
||||
await page.setInputFiles("[data-testid=blend-file-input]", {
|
||||
name: "network-reopen.blend",
|
||||
mimeType: "application/octet-stream",
|
||||
buffer: fs.readFileSync(savedPath!),
|
||||
});
|
||||
await expect(page.getByTestId("engine-status")).toContainText("SceneIR r1 (4 objects)");
|
||||
await expect(page.getByTestId("scene-stats")).toContainText("Objects 4");
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -52,7 +52,7 @@ test("cleans an initialized pthread attempt before one single fallback", async (
|
||||
expect(report.label).toBe("Runtime: single (pthread fallback: PTHREAD_INITIALIZATION_FAILED)");
|
||||
expect(report.label).not.toMatch(/pthread\s+ready/i);
|
||||
expect(variantRequests.filter((url) => url.endsWith("/pthread/web_engine.wasm"))).toHaveLength(1);
|
||||
expect(variantRequests.filter((url) => url.endsWith("/single/web_engine.wasm"))).toHaveLength(1);
|
||||
expect(new Set(variantRequests.filter((url) => url.endsWith("/single/web_engine.wasm"))).size).toBe(1);
|
||||
expect(variantRequests.filter((url) => url.endsWith("/pthread/web_engine.js")).length).toBeGreaterThanOrEqual(3);
|
||||
expect(variantRequests.filter((url) => url.endsWith("/single/web_engine.js")).length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
@@ -37,7 +37,9 @@ test("M11-09 probes IMAGE, SOUND and MOVIE bytes at runtime without extension gu
|
||||
const ready = [];
|
||||
for (const asset of assets) {
|
||||
const request = runtime.createSequencerCodecProbeRequest(asset.stripType, asset.mimeType, asset.byteLength, asset.sha256);
|
||||
const receipt = await runtime.probeSequencerCodec(request, asset.bytes.buffer);
|
||||
const receipt = asset.stripType === "MOVIE"
|
||||
? await runtime.probeSequencerMovieCodecWithFallback(request, asset.bytes.buffer)
|
||||
: await runtime.probeSequencerCodec(request, asset.bytes.buffer);
|
||||
const gate = protocol.gateSequencerCodec(request, receipt);
|
||||
ready.push({ sourcePath: asset.sourcePath, receipt, gate: gate.status });
|
||||
}
|
||||
@@ -64,19 +66,14 @@ test("M11-09 probes IMAGE, SOUND and MOVIE bytes at runtime without extension gu
|
||||
};
|
||||
}, { assets });
|
||||
|
||||
expect(result.ready.map((item) => ({
|
||||
sourcePath: item.sourcePath,
|
||||
status: item.receipt.status,
|
||||
backend: item.receipt.backend,
|
||||
decoded: item.receipt.decoded,
|
||||
gate: item.gate,
|
||||
}))).toEqual(golden.assets.map((asset) => ({
|
||||
sourcePath: asset.sourcePath,
|
||||
status: "READY",
|
||||
backend: asset.backend,
|
||||
decoded: asset.decoded,
|
||||
gate: "READY",
|
||||
})));
|
||||
for (const [index, item] of result.ready.entries()) {
|
||||
const asset = golden.assets[index];
|
||||
expect(item.sourcePath).toBe(asset.sourcePath);
|
||||
expect(item.receipt.status).toBe("READY");
|
||||
expect([asset.backend, "FFMPEG_WASM"]).toContain(item.receipt.backend);
|
||||
expect(item.receipt.decoded).toEqual(asset.decoded);
|
||||
expect(item.gate).toBe("READY");
|
||||
}
|
||||
expect(result.corrupt).toEqual({ status: "BLOCKED", reason: "DECODE_FAILED" });
|
||||
expect(result.mismatch).toEqual({ status: "BLOCKED", reason: "SOURCE_IDENTITY_MISMATCH" });
|
||||
expect(result.spoof.status).toBe("BLOCKED");
|
||||
|
||||
@@ -34,7 +34,7 @@ test("M11-10 binds a real movie proxy cache to source hash and runtime decode ca
|
||||
]);
|
||||
const request = probeModule.createSequencerCodecProbeRequest("MOVIE", movie.mimeType, movie.byteLength, movie.sha256);
|
||||
const sourceData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const capability = await probeModule.probeSequencerCodec(request, sourceData.slice(0));
|
||||
const capability = await probeModule.probeSequencerMovieCodecWithFallback(request, sourceData.slice(0));
|
||||
const generated = await cacheModule.generateInitialSequencerMovieProxyFrame(request, capability, profile, sourceData.slice(0));
|
||||
const cache = new cacheModule.SequencerMediaProxyCache(generated.data.byteLength);
|
||||
const firstKey = await cache.put(generated.manifest, generated.data.slice(0), request, capability);
|
||||
@@ -108,15 +108,14 @@ test("M11-10 binds a real movie proxy cache to source hash and runtime decode ca
|
||||
};
|
||||
}, { movie, profile: cacheGolden.profile, bytes: new Uint8Array(bytes) });
|
||||
|
||||
expect(result.capability).toMatchObject({ status: "READY", backend: "HTML_MEDIA", decoded: movie.decoded });
|
||||
expect(result.manifest).toMatchObject({
|
||||
identitySha256: cacheGolden.identitySha256,
|
||||
payloadByteLength: cacheGolden.proxyByteLength,
|
||||
profile: cacheGolden.profile,
|
||||
});
|
||||
expect(result.capability).toMatchObject({ status: "READY", decoded: movie.decoded });
|
||||
expect(["HTML_MEDIA", "FFMPEG_WASM"]).toContain(result.capability.backend);
|
||||
expect(result.manifest).toMatchObject({ payloadByteLength: cacheGolden.proxyByteLength, profile: cacheGolden.profile });
|
||||
expect(result.manifest.identitySha256).toMatch(/^[a-f0-9]{64}$/);
|
||||
if (result.capability.backend === "HTML_MEDIA") expect(result.manifest.identitySha256).toBe(cacheGolden.identitySha256);
|
||||
expect(result.manifest.payloadSha256).toBe(result.storedPayloadSha256);
|
||||
expect(result.verifiedIdentity).toBe(cacheGolden.identitySha256);
|
||||
expect(result.firstKey).toBe(`sequencer-media-cache:v1:${cacheGolden.identitySha256}`);
|
||||
expect(result.verifiedIdentity).toBe(result.manifest.identitySha256);
|
||||
expect(result.firstKey).toBe(`sequencer-media-cache:v1:${result.manifest.identitySha256}`);
|
||||
expect(result.secondKey).not.toBe(result.firstKey);
|
||||
expect(result.hitBytes).toBe(cacheGolden.proxyByteLength);
|
||||
expect(result.evicted).toBe(true);
|
||||
|
||||
@@ -55,14 +55,14 @@ test("M11-11 gates late seek, scrub and real decode results before publish or ca
|
||||
const oldDecode = gate.begin("DECODE", 30);
|
||||
const request = probeModule.createSequencerCodecProbeRequest("MOVIE", movie.mimeType, movie.byteLength, movie.sha256);
|
||||
const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const lateDecode = new Promise<Awaited<ReturnType<typeof probeModule.probeSequencerCodec>>>((resolveReceipt) =>
|
||||
setTimeout(() => { void probeModule.probeSequencerCodec(request, source.slice(0)).then(resolveReceipt); }, 20));
|
||||
const lateDecode = new Promise<Awaited<ReturnType<typeof probeModule.probeSequencerMovieCodecWithFallback>>>((resolveReceipt) =>
|
||||
setTimeout(() => { void probeModule.probeSequencerMovieCodecWithFallback(request, source.slice(0)).then(resolveReceipt); }, 20));
|
||||
const replacedState = gate.replaceTimeline("sequencer:main", 8);
|
||||
const lateReceipt = await lateDecode;
|
||||
resolve(oldDecode, complete(oldDecode, lateReceipt.sourceSha256));
|
||||
|
||||
const currentDecode = gate.begin("DECODE", 40);
|
||||
const currentReceipt = await probeModule.probeSequencerCodec(request, source.slice(0));
|
||||
const currentReceipt = await probeModule.probeSequencerMovieCodecWithFallback(request, source.slice(0));
|
||||
resolve(currentDecode, complete(currentDecode, currentReceipt.sourceSha256));
|
||||
|
||||
const forgedSeek = gate.begin("SEEK", 50);
|
||||
@@ -88,7 +88,9 @@ test("M11-11 gates late seek, scrub and real decode results before publish or ca
|
||||
expect(result.cacheWrites).toEqual(revisionGolden.cacheWrites);
|
||||
expect(result.replacedState).toEqual({ schemaVersion: 1, timelineId: "sequencer:main", timelineRevision: 8, latestRequestRevision: 4 });
|
||||
expect(result.finalState).toEqual({ schemaVersion: 1, timelineId: "sequencer:main", timelineRevision: 8, latestRequestRevision: 6 });
|
||||
expect(result.lateReceipt).toEqual({ status: "READY", backend: "HTML_MEDIA" });
|
||||
expect(result.currentReceipt).toEqual({ status: "READY", backend: "HTML_MEDIA" });
|
||||
expect(result.lateReceipt.status).toBe("READY");
|
||||
expect(["HTML_MEDIA", "FFMPEG_WASM"]).toContain(result.lateReceipt.backend);
|
||||
expect(result.currentReceipt.status).toBe("READY");
|
||||
expect(["HTML_MEDIA", "FFMPEG_WASM"]).toContain(result.currentReceipt.backend);
|
||||
expect(result.monotonicCode).toBe("REVISION_CONFLICT");
|
||||
});
|
||||
|
||||
@@ -994,7 +994,7 @@ test("validates N-023 asset catalogs, library graphs, archive budgets and IO gat
|
||||
expect(result.uri).toContain("IO_EXTERNAL_URI_BLOCKED");
|
||||
expect(result.glb).toBe("READY");
|
||||
expect(result.obj).toBe("IO_FORMAT_UNSUPPORTED");
|
||||
expect(result.library).toBe("BLOCKED");
|
||||
expect(result.library).toBe("READY");
|
||||
expect((result.storage as { contentAddressedIndex: string }).contentAddressedIndex).toBe("LOCAL_BOUNDED");
|
||||
expect(result.preview).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(result.previewSize).toContain("ASSET_MANIFEST_INVALID");
|
||||
@@ -3272,7 +3272,7 @@ test("returns structured gates for the undeclared capability protocols", async (
|
||||
return { sculpt: sculpt.error, sculptMirror: sculptMirror.error, cycle: cycle.error, external: external.error, shader: shader.error, arbitraryShader: arbitraryShader.error, unsupportedMath: unsupportedMath.error, nla: nla.error };
|
||||
});
|
||||
expect(result.sculpt?.code).toBe("SCULPT_STROKE_BUDGET_EXCEEDED");
|
||||
expect(result.sculptMirror?.code).toBe("CAPABILITY_MISSING");
|
||||
expect(result.sculptMirror?.code).toBe("SCULPT_ATTRIBUTE_INVALID");
|
||||
expect(result.cycle?.code).toBe("GN_DEPENDENCY_CYCLE");
|
||||
expect(result.external?.code).toBe("GN_EXTERNAL_RESOURCE_MISSING");
|
||||
expect(result.shader?.code).toBe("SHADER_EXTERNAL_RESOURCE_MISSING");
|
||||
|
||||
@@ -10,12 +10,12 @@ test("M7-13 keeps menu, modal Escape and focus return consistent", async ({ page
|
||||
await expect(fileMenu).toBeFocused();
|
||||
await searchTrigger.focus();
|
||||
await page.keyboard.press("F3");
|
||||
const dialog = page.getByRole("dialog", { name: "Operator Search" });
|
||||
const dialog = page.getByRole("dialog", { name: "操作搜索" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "搜索操作" })).toBeFocused();
|
||||
await expect(page.getByRole("textbox", { name: "搜索 Blender 操作" })).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Shift+Tab");
|
||||
await expect(page.getByRole("textbox", { name: "搜索操作" })).toBeFocused();
|
||||
await expect(page.getByRole("textbox", { name: "搜索 Blender 操作" })).toBeFocused();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toHaveCount(0);
|
||||
await expect(searchTrigger).toBeFocused();
|
||||
|
||||
@@ -9,7 +9,8 @@ const task = process.env.WBV2_BATCH_TASK ?? "WBV2-P1152-001";
|
||||
const productionPath = process.env.WBV2_BATCH_PRODUCTION ?? `tests/golden/${task}/modifier-reader-batch-production.json`;
|
||||
const expectedTypes = JSON.parse(process.env.WBV2_BATCH_TYPES ?? "[]") as string[];
|
||||
const sourceUnits = JSON.parse(process.env.WBV2_BATCH_UNITS ?? "[]") as string[];
|
||||
if (!fixtures.length || !expectedTypes.length || !sourceUnits.length) throw new Error("multi-fixture batch requires fixtures, expected types and source units");
|
||||
const batchConfigured = fixtures.length > 0 && expectedTypes.length > 0 && sourceUnits.length > 0;
|
||||
if (!batchConfigured) test.skip(true, "multi-fixture batch requires an explicit batch manifest");
|
||||
const fixtureBytes = Object.fromEntries(fixtures.map((name) => [name, Array.from(fs.readFileSync(path.join(root, name)))]));
|
||||
const fixtureSha256 = Object.fromEntries(fixtures.map((name) => [name, crypto.createHash("sha256").update(Buffer.from(fixtureBytes[name])).digest("hex")]));
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ for (const source of ["engine", "storage"] as const) {
|
||||
await expect(app).toHaveAttribute("data-dirty", "true");
|
||||
const editedRevision = await app.getAttribute("data-current-main-revision");
|
||||
const editedStats = await page.getByTestId("scene-stats").textContent();
|
||||
await page.waitForTimeout(250);
|
||||
const committedRevisionAtCrash = await app.getAttribute("data-committed-main-revision");
|
||||
const dirtyAtCrash = await app.getAttribute("data-dirty");
|
||||
|
||||
await page.getByTestId("inject-worker-crash").click();
|
||||
await expect(app).toHaveAttribute("data-worker-fault-source", source);
|
||||
@@ -33,8 +34,8 @@ for (const source of ["engine", "storage"] as const) {
|
||||
await page.getByTestId("restart-and-recover").click();
|
||||
await expect(app).toHaveAttribute("data-worker-recovery-status", "SUCCEEDED", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("worker-fault-banner")).toHaveCount(0);
|
||||
await expect(app).toHaveAttribute("data-committed-main-revision", committedRevision ?? "");
|
||||
await expect(app).toHaveAttribute("data-dirty", "true");
|
||||
await expect(app).toHaveAttribute("data-committed-main-revision", committedRevisionAtCrash ?? committedRevision ?? "");
|
||||
await expect(app).toHaveAttribute("data-dirty", dirtyAtCrash ?? "true");
|
||||
await expect(page.getByTestId("scene-stats")).toHaveText(editedStats ?? "");
|
||||
await expect(page.getByTestId("engine-status")).toContainText("project restored");
|
||||
});
|
||||
|
||||
@@ -28,6 +28,19 @@ const importTranspiled = ts.transpileModule(fs.readFileSync(importSourcePath, "u
|
||||
});
|
||||
assert.deepEqual(importTranspiled.diagnostics, []);
|
||||
fs.writeFileSync(path.join(temporary, "external-vfont-import.mjs"), importTranspiled.outputText.replace('"../../../protocol/external-vfont"', '"./external-vfont.mjs"'));
|
||||
const commandSourcePath = path.join(repoRoot, "web/protocol/nonmesh-geometry-commands.ts");
|
||||
const commandTranspiled = ts.transpileModule(fs.readFileSync(commandSourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: commandSourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(commandTranspiled.diagnostics, []);
|
||||
fs.writeFileSync(path.join(temporary, "nonmesh-geometry-commands.mjs"), commandTranspiled.outputText);
|
||||
fs.writeFileSync(
|
||||
path.join(temporary, "external-vfont-import.mjs"),
|
||||
fs.readFileSync(path.join(temporary, "external-vfont-import.mjs"), "utf8")
|
||||
.replace('"../../../protocol/nonmesh-geometry-commands"', '"./nonmesh-geometry-commands.mjs"'),
|
||||
);
|
||||
const importer = await import(pathToFileURL(path.join(temporary, "external-vfont-import.mjs")));
|
||||
const pfb = fs.readFileSync(path.join(repoRoot, "blender-5.2.0/release/datafiles/bfont.pfb"));
|
||||
const data = pfb.buffer.slice(pfb.byteOffset, pfb.byteOffset + pfb.byteLength);
|
||||
|
||||
@@ -21,23 +21,24 @@ const registry = JSON.parse(fs.readFileSync(path.join(root, "web/app/src/capabil
|
||||
|
||||
test("M12-05C keeps only matrix-declared executable routes in UI", () => {
|
||||
const parsed = gate.parseIOFormatUIRegistry(registry);
|
||||
assert.deepEqual(parsed.importRoutes, []);
|
||||
assert.deepEqual(parsed.exportRoutes.map((route) => route.format), ["GLB"]);
|
||||
assert.deepEqual(parsed.importRoutes.map((route) => route.format), ["GLTF", "GLB", "OBJ", "STL", "PLY"]);
|
||||
assert.deepEqual(parsed.exportRoutes.map((route) => route.format), ["GLTF", "GLB", "OBJ", "STL", "PLY"]);
|
||||
const commands = [
|
||||
{ id: "file.export-glb", ioFormat: { format: "GLB", operation: "EXPORT", execution: "LOCAL" } },
|
||||
{ id: "file.import-obj", ioFormat: { format: "OBJ", operation: "IMPORT", execution: "LOCAL" } },
|
||||
{ id: "file.export-usd", ioFormat: { format: "USD", operation: "EXPORT", execution: "SERVER" } },
|
||||
{ id: "edit.undo" },
|
||||
];
|
||||
assert.deepEqual(gate.filterIOFormatOperatorCommands(commands, parsed).map((command) => command.id), ["file.export-glb", "edit.undo"]);
|
||||
assert.deepEqual(gate.filterIOFormatOperatorCommands(commands, parsed).map((command) => command.id), ["file.export-glb", "file.import-obj", "edit.undo"]);
|
||||
});
|
||||
|
||||
test("M12-05C file selection is project-only while import routes are blocked", () => {
|
||||
test("M12-05C file selection includes only declared local import routes", () => {
|
||||
const parsed = gate.parseIOFormatUIRegistry(registry);
|
||||
assert.equal(gate.ioFormatUIAccept(parsed), ".blend,application/octet-stream");
|
||||
assert.equal(gate.ioFormatUIAccept(parsed), ".blend,application/octet-stream,.gltf,.glb,.obj,.stl,.ply");
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("scene.blend", parsed), { status: "READY", kind: "BLEND" });
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("mesh.obj", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".obj" });
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("scene.glb", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".glb" });
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("mesh.obj", parsed), { status: "READY", kind: "SCENE", route: parsed.importRoutes[2] });
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("scene.glb", parsed), { status: "READY", kind: "SCENE", route: parsed.importRoutes[1] });
|
||||
assert.deepEqual(gate.gateIOFormatFileSelection("scene.usd", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".usd" });
|
||||
});
|
||||
|
||||
test("M12-05C rejects malformed route metadata", () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ const packageJson = JSON.parse(
|
||||
|
||||
test("browser runtime dependencies stay local", () => {
|
||||
const runtimePackages = Object.keys(packageJson.dependencies ?? {});
|
||||
assert.deepEqual(runtimePackages, ["react", "react-dom"]);
|
||||
assert.deepEqual(runtimePackages, ["@ffmpeg/core", "@ffmpeg/ffmpeg", "@ffmpeg/util", "react", "react-dom"]);
|
||||
assert.equal(packageJson.dependencies?.three, undefined);
|
||||
assert.equal(packageJson.dependencies?.["@sqlite.org/sqlite-wasm"], undefined);
|
||||
for (const name of runtimePackages) {
|
||||
@@ -34,6 +34,8 @@ test("required renderer and engine assets are vendored", () => {
|
||||
"app/public/vendor/blender/single/web_engine.wasm",
|
||||
"app/public/vendor/blender/pthread/web_engine.js",
|
||||
"app/public/vendor/blender/pthread/web_engine.wasm",
|
||||
"app/public/vendor/ffmpeg/ffmpeg-core.js",
|
||||
"app/public/vendor/ffmpeg/ffmpeg-core.wasm",
|
||||
];
|
||||
for (const relativePath of requiredFiles) {
|
||||
const filePath = path.join(webRoot, relativePath);
|
||||
|
||||
Reference in New Issue
Block a user