import { expect, test } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { computeLibraryAppendClosureSha256, LIBRARY_MAIN_APPEND_SCHEMA } 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 source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend")); const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend")); test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const closure = { object: "Object/M12 Append Object", mesh: "Mesh/M12 Append Mesh", material: "Material/M12 Append Material", image: "Image/M12 Append Image", } as const; const sourceSha256 = crypto.createHash("sha256").update(source).digest("hex"); const sourceIdentity = await createLibrarySourceIdentity({ sourceLocator: "project-assets/libraries/m12_append_source.blend", sourceSha256, }); 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:m12-03d", localDataBlockId: closure.object }, readOnly: false, referenceReadOnly: false, sourceGeneration: 1, sourceRevision: 0, dependencyClosureSha256, }); const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const sourceBuffer = Uint8Array.from(sourceBytes).buffer; const targetBuffer = Uint8Array.from(targetBytes).buffer; const digest = async (bytes: ArrayBuffer): Promise => { const hash = await crypto.subtle.digest("SHA-256", bytes); return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join(""); }; const makeRequest = (baseRevision: number) => ({ schemaVersion: 1 as const, baseRevision, binding, expectedClosure: closure, }); const client = new WebEngineClient({ timeoutMs: 60_000 }); const reopened = new WebEngineClient({ timeoutMs: 60_000 }); const ids = { object: "object:M12 Append Object", mesh: "mesh:M12 Append Mesh", material: "material:M12 Append Material", image: "image:M12 Append Image", }; const closureState = (snapshot: any) => ({ object: snapshot.nodes.find((item: any) => item.id === ids.object), mesh: snapshot.meshes.find((item: any) => item.id === ids.mesh), material: snapshot.materials.find((item: any) => item.id === ids.material), image: snapshot.images.find((item: any) => item.id === ids.image), }); try { const opened = await client.openBlend(targetBuffer.slice(0)); const baseRevision = opened.snapshot.revision; const request = await makeRequest(baseRevision); const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request); const appendedClosure = closureState(appended.snapshot); const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision)) .then(() => ({ code: "NO_ERROR" })) .catch((error: any) => ({ code: error.code, message: error.message })); const collisionRequest = await makeRequest(appended.snapshot.revision); const collision = await client.appendLibraryObject(sourceBuffer.slice(0), collisionRequest) .then(() => ({ code: "NO_ERROR" })) .catch((error: any) => ({ code: error.code, message: error.message })); const afterCollision = await client.snapshot(); const undone = await client.applyCommand({ type: "undo" }); const redone = await client.applyCommand({ type: "redo" }); const saved = await client.saveBlend(); const reopenedResult = await reopened.openBlend(saved); const reopenedClosure = closureState(reopenedResult.snapshot); return { sourceSha256: await digest(sourceBuffer), baseRevision, appendedRevision: appended.snapshot.revision, receipt: appended.receipt, delta: appended.delta, appendedClosure: { object: appendedClosure.object && { type: appendedClosure.object.type, dataId: appendedClosure.object.dataId }, mesh: appendedClosure.mesh && { vertexCount: appendedClosure.mesh.vertexCount, materialSlotIds: appendedClosure.mesh.materialSlotIds }, material: appendedClosure.material && { imageIds: appendedClosure.material.imageIds ?? appendedClosure.material.nodes?.flatMap((node: any) => node.imageId ? [node.imageId] : []), }, image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height }, }, stale, collision, collisionRevision: afterCollision.snapshot.revision, undoHasObject: Boolean(closureState(undone.snapshot).object), redoHasObject: Boolean(closureState(redone.snapshot).object), reopenedRevision: reopenedResult.snapshot.revision, reopenedHasObject: Boolean(reopenedClosure.object), reopenedHasLocalImage: reopenedClosure.image?.libraryLinked === false, }; } catch (error: any) { return { error: { code: error?.code, message: error?.message, detail: error?.detail } }; } finally { client.terminate(); reopened.terminate(); } }, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding }); expect(result.error).toBeUndefined(); expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/); expect(result.appendedRevision).toBe(result.baseRevision + 1); expect(result.receipt).toMatchObject({ operation: "APPEND", transactionCount: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision }); expect(result.receipt.mapping).toHaveLength(4); expect(result.receipt.mapping.every((item: any) => item.owner === "LOCAL_MAIN" && item.readOnly === false && item.source === item.local)).toBe(true); expect(result.delta).toMatchObject({ schemaVersion: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision }); expect(result.appendedClosure).toEqual({ object: { type: "MESH", dataId: "mesh:M12 Append Mesh" }, mesh: { vertexCount: 4, materialSlotIds: ["material:M12 Append Material"] }, material: { imageIds: ["image:M12 Append Image"] }, image: { libraryLinked: false, width: 2, height: 2 }, }); expect(result.stale.code).toBe("REVISION_CONFLICT"); expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID"); expect(result.collisionRevision).toBe(result.appendedRevision); expect(result.undoHasObject).toBe(false); expect(result.redoHasObject).toBe(true); expect(result.reopenedHasObject).toBe(true); expect(result.reopenedHasLocalImage).toBe(true); });