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 => { 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); });