Files
workinf_Blender_Wasm/web/tests/e2e/wbv2-native-engine-production.spec.ts
mes123456 b78e23b16c
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance WBV2 through native engine module matrix
2026-08-28 00:46:58 +08:00

150 lines
12 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
import { computeLibraryAppendClosureSha256 } from "../../protocol/library-main-append";
import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPERATION_IDENTITY_SCHEMA } from "../../protocol/library-operation-identity";
const root = path.resolve(import.meta.dirname, "../../..");
const evidenceRoot = path.join(root, "tests/golden/WBV2-P10-002");
const meshFixturePath = path.join(root, "tests/files/web/generated/WBV2-P2-003-MESH.EDIT_CORE.blend");
const modifierFixturePath = path.join(root, "tests/files/web/generated/WBV2-P4-001-MODIFIER.DECLARED_SLICES.blend");
const assetTargetPath = path.join(root, "tests/files/web/generated/WBV2-P3-005-ASSET.LIBRARY_OPERATIONS.blend");
const assetSourcePath = path.join(root, "tests/files/web/generated/WBV2P3005Library.blend");
const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
test("WBV2-P10-002 reaches the compiled Blender WASM Main through Chromium", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("/");
const meshBytes = fs.readFileSync(meshFixturePath);
const modifierBytes = fs.readFileSync(modifierFixturePath);
const assetTargetBytes = fs.readFileSync(assetTargetPath);
const assetSourceBytes = fs.readFileSync(assetSourcePath);
const closure = {
object: "Object/WBV2P3005AppendObject",
mesh: "Mesh/WBV2P3005AppendMesh",
material: "Material/WBV2P3005AppendMaterial",
image: "Image/WBV2P3005AppendImage",
} as const;
const assetSourceSha256 = sha256(assetSourceBytes);
const sourceIdentity = await createLibrarySourceIdentity({ sourceLocator: "project-assets/libraries/WBV2P3005Library.blend", sourceSha256: assetSourceSha256 });
const dependencyClosureSha256 = await computeLibraryAppendClosureSha256(closure);
const binding = await createLibraryOperationBinding({
schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA,
operation: "APPEND",
source: sourceIdentity,
sourceDataBlockId: closure.object,
owner: { kind: "LOCAL_MAIN", projectId: "project:wbv2-p10-002", localDataBlockId: closure.object },
readOnly: false,
referenceReadOnly: false,
sourceGeneration: 1,
sourceRevision: 0,
dependencyClosureSha256,
});
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const bytes = (values: number[]): ArrayBuffer => Uint8Array.from(values).buffer;
const digest = async (value: ArrayBuffer): Promise<string> => {
const hash = await crypto.subtle.digest("SHA-256", value);
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
};
const position = (result: any, meshId: string): number[] => {
const buffer = result.geometryBuffers.find((candidate: any) => candidate.meshId === meshId);
if (!buffer) throw new Error(`missing geometry buffer ${meshId}`);
return Array.from(new Float32Array(buffer.positions).slice(0, 3));
};
const errorShape = (error: any): { code?: string; message?: string } => ({ code: error?.code, message: error?.message });
const runMesh = async () => {
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const opened = await client.openBlend(bytes(input.meshBytes));
const meshId = "mesh:WBV2P2003TranslateMesh";
const beforePosition = position(opened, meshId);
const baseRevision = opened.snapshot.revision;
const edited = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [0], offset: [0.25, -0.5, 1], baseRevision } as any);
const stale = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [0], offset: [0.25, -0.5, 1], baseRevision } as any).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const malformed = await client.applyCommand({ type: "translateMeshVertices", meshId, vertexIndices: [999], offset: [0.25, -0.5, 1], baseRevision: edited.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const saved = await client.saveBlend();
const savedSha256 = await digest(saved.slice(0));
const reopened = await reopenedClient.openBlend(saved.slice(0));
return { baseRevision, editedRevision: edited.snapshot.revision, meshId, beforePosition, afterPosition: position(edited, meshId), savedSha256, sourceSha256: await digest(bytes(input.meshBytes)), reopenedRevision: reopened.snapshot.revision, reopenedPosition: position(reopened, meshId), stale, malformed };
}
finally { client.terminate(); reopenedClient.terminate(); }
};
const runModifier = async () => {
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const opened = await client.openBlend(bytes(input.modifierBytes));
const mesh = opened.snapshot.meshes.find((candidate: any) => candidate.modifierStack?.length);
const modifier = mesh?.modifierStack?.find((candidate: any) => candidate.type === "BEVEL") ?? mesh?.modifierStack?.[0];
if (!mesh || !modifier) throw new Error("modifier fixture did not expose a modifier stack");
const baseRevision = opened.snapshot.revision;
const enabled = !modifier.enabled;
const edited = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: modifier.uuid, enabled, baseRevision } as any);
const stale = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: modifier.uuid, enabled: !enabled, baseRevision } as any).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const malformed = await client.applyCommand({ type: "setModifierEnabled", meshId: mesh.id, modifierUuid: "modifier:missing", enabled, baseRevision: edited.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const saved = await client.saveBlend();
const savedSha256 = await digest(saved.slice(0));
const reopened = await reopenedClient.openBlend(saved.slice(0));
const reopenedMesh = reopened.snapshot.meshes.find((candidate: any) => candidate.id === mesh.id);
const reopenedModifier = reopenedMesh?.modifierStack?.find((candidate: any) => candidate.uuid === modifier.uuid);
return { baseRevision, editedRevision: edited.snapshot.revision, meshId: mesh.id, modifierUuid: modifier.uuid, beforeEnabled: modifier.enabled, afterEnabled: edited.snapshot.meshes.find((candidate: any) => candidate.id === mesh.id)?.modifierStack?.find((candidate: any) => candidate.uuid === modifier.uuid)?.enabled, savedSha256, sourceSha256: await digest(bytes(input.modifierBytes)), reopenedRevision: reopened.snapshot.revision, reopenedEnabled: reopenedModifier?.enabled, stale, malformed };
}
finally { client.terminate(); reopenedClient.terminate(); }
};
const runAsset = async () => {
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const target = await client.openBlend(bytes(input.assetTargetBytes));
const baseRevision = target.snapshot.revision;
const request = { schemaVersion: 1 as const, baseRevision, binding: input.binding, expectedClosure: input.closure };
const appended = await client.appendLibraryObject(bytes(input.assetSourceBytes), request);
const stale = await client.appendLibraryObject(bytes(input.assetSourceBytes), request).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const collisionRequest = { ...request, baseRevision: appended.snapshot.revision };
const collision = await client.appendLibraryObject(bytes(input.assetSourceBytes), collisionRequest).then(() => ({ code: "NO_ERROR" })).catch(errorShape);
const afterCollision = await client.snapshot();
const saved = await client.saveBlend();
const savedSha256 = await digest(saved.slice(0));
const reopened = await reopenedClient.openBlend(saved.slice(0));
const closureState = (snapshot: any) => ({ object: Boolean(snapshot.nodes.find((candidate: any) => candidate.id === "object:WBV2P3005AppendObject")), mesh: Boolean(snapshot.meshes.find((candidate: any) => candidate.id === "mesh:WBV2P3005AppendMesh")), material: Boolean(snapshot.materials.find((candidate: any) => candidate.id === "material:WBV2P3005AppendMaterial")), image: snapshot.images.find((candidate: any) => candidate.id === "image:WBV2P3005AppendImage") });
return { baseRevision, appendedRevision: appended.snapshot.revision, sourceSha256: await digest(bytes(input.assetSourceBytes)), targetSha256: await digest(bytes(input.assetTargetBytes)), receipt: appended.receipt, delta: appended.delta, appendedClosure: closureState(appended.snapshot), stale, collision, collisionRevision: afterCollision.snapshot.revision, savedSha256, reopenedRevision: reopened.snapshot.revision, reopenedClosure: closureState(reopened.snapshot) };
}
finally { client.terminate(); reopenedClient.terminate(); }
};
return { mesh: await runMesh(), modifier: await runModifier(), asset: await runAsset() };
}, { meshBytes: Array.from(meshBytes), modifierBytes: Array.from(modifierBytes), assetTargetBytes: Array.from(assetTargetBytes), assetSourceBytes: Array.from(assetSourceBytes), closure, binding });
fs.mkdirSync(evidenceRoot, { recursive: true });
const output = process.env.WBV2_NATIVE_ENGINE_REPORT ?? "native-engine-production.json";
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P10-002", operation: "NATIVE_WASM_ENGINE_PRODUCTION_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
expect(result.mesh.baseRevision).toBeGreaterThan(0);
expect(result.mesh.editedRevision).toBe(result.mesh.baseRevision + 1);
expect(result.mesh.afterPosition).toEqual(result.mesh.beforePosition.map((value, index) => value + [0.25, -0.5, 1][index]));
expect(result.mesh.reopenedPosition).toEqual(result.mesh.afterPosition);
expect(result.mesh.savedSha256).not.toBe(result.mesh.sourceSha256);
expect(result.mesh.stale.message).toContain("REVISION_CONFLICT");
expect(result.mesh.malformed.code).toBe("INVALID_ARGUMENT");
expect(result.modifier.editedRevision).toBe(result.modifier.baseRevision + 1);
expect(result.modifier.afterEnabled).toBe(!result.modifier.beforeEnabled);
expect(result.modifier.reopenedEnabled).toBe(result.modifier.afterEnabled);
expect(result.modifier.savedSha256).not.toBe(result.modifier.sourceSha256);
expect(result.modifier.stale.message).toContain("REVISION_CONFLICT");
expect(result.modifier.malformed.code).toBe("INVALID_ARGUMENT");
expect(result.asset.appendedRevision).toBe(result.asset.baseRevision + 1);
expect(result.asset.receipt.operation).toBe("APPEND");
expect(result.asset.receipt.transactionCount).toBe(1);
expect(result.asset.appendedClosure.object).toBe(true);
expect(result.asset.appendedClosure.mesh).toBe(true);
expect(result.asset.appendedClosure.material).toBe(true);
expect(result.asset.appendedClosure.image.libraryLinked).toBe(false);
expect(result.asset.appendedClosure.image.packed).toBe(true);
expect(result.asset.reopenedClosure).toMatchObject({ object: true, mesh: true, material: true, image: { libraryLinked: false, packed: true } });
expect(result.asset.stale.message).toContain("REVISION_CONFLICT");
expect(result.asset.collision.code).toBe("ASSET_MANIFEST_INVALID");
expect(result.asset.collisionRevision).toBe(result.asset.appendedRevision);
});