import { expect, test } from "@playwright/test"; test("M9-11 atomically publishes packed and UDIM dirty tiles after verified OPFS writes", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const result = await page.evaluate(async () => { const { StorageClient } = await import("/src/storage/StorageClient.ts"); const projectId = "m9-texture-paint-atomic"; const hash = async (bytes: Uint8Array | ArrayBuffer) => { const data = bytes instanceof Uint8Array ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : bytes; return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", data)), (value) => value.toString(16).padStart(2, "0")).join(""); }; const encode = async (pixels: Uint8Array) => { const canvas = new OffscreenCanvas(2, 2); const context = canvas.getContext("2d")!; context.putImageData(new ImageData(new Uint8ClampedArray(pixels), 2, 2), 0, 0); return (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer(); }; const decode = async (data: ArrayBuffer) => { const bitmap = await createImageBitmap(new Blob([data], { type: "image/png" })); const canvas = new OffscreenCanvas(2, 2); const context = canvas.getContext("2d", { willReadFrequently: true })!; context.drawImage(bitmap, 0, 0); bitmap.close(); return new Uint8Array(context.getImageData(0, 0, 2, 2).data); }; const commit = async ({ client, textureAssetId, kind, tile, revision, sourcePath, baseAssetSha256, basePixels, offset, dirty, faultAt, }: { client: InstanceType; textureAssetId: string; kind: "PACKED" | "UDIM"; tile: number; revision: number; sourcePath: string; baseAssetSha256: string; basePixels: Uint8Array; offset: number; dirty: number[]; faultAt?: "after-asset-write"; }) => { const next = basePixels.slice(); next.set(dirty, offset); return client.commitTexturePaintTile({ schemaVersion: 1, target: { schemaVersion: 1, projectId, imageId: "image:M9Paint", textureAssetId, kind, tile, revision, width: 2, height: 2, mimeType: "image/png", colorSpace: "SRGB", sourcePath, baseAssetSha256, }, patch: { schemaVersion: 1, textureAssetId, tile, revision, width: 2, height: 2, format: "RGBA8", colorSpace: "SRGB", baseSha256: await hash(basePixels), resultSha256: await hash(next), byteOffset: offset, bytes: new Uint8Array(dirty), }, faultAt, }); }; const opaqueBlack = new Uint8Array(Array.from({ length: 4 }, () => [0, 0, 0, 255]).flat()); const client = new StorageClient(); const packedBase = await client.putAsset(projectId, await encode(opaqueBlack), "image/png", "textures/packed.png"); const packedId = "image:M9Paint:packed"; const packedFirstPixels = opaqueBlack.slice(); packedFirstPixels.set([255, 0, 0, 255], 0); const packedFirst = await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 4, sourcePath: "textures/packed.png", baseAssetSha256: packedBase.sha256, basePixels: opaqueBlack, offset: 0, dirty: [255, 0, 0, 255] }); const firstAsset = await client.readAsset(projectId, packedFirst.binding.assetSha256); const firstPixels = await decode(firstAsset.data); let injected = ""; try { await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 5, sourcePath: "textures/packed.png", baseAssetSha256: packedFirst.binding.assetSha256, basePixels: packedFirstPixels, offset: 4, dirty: [0, 255, 0, 255], faultAt: "after-asset-write" }); } catch (error) { injected = error instanceof Error ? error.message : String(error); } const afterFailure = await client.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: packedId, tile: 1001 }); const packedSecondPixels = packedFirstPixels.slice(); packedSecondPixels.set([0, 255, 0, 255], 4); const packedSecond = await commit({ client, textureAssetId: packedId, kind: "PACKED", tile: 1001, revision: 5, sourcePath: "textures/packed.png", baseAssetSha256: packedFirst.binding.assetSha256, basePixels: packedFirstPixels, offset: 4, dirty: [0, 255, 0, 255] }); const udimBase = await client.putAsset(projectId, await encode(opaqueBlack), "image/png", "textures/paint.1002.png"); const udimId = "image:M9Paint:tile:1002"; const udim = await commit({ client, textureAssetId: udimId, kind: "UDIM", tile: 1002, revision: 5, sourcePath: "textures/paint.1002.png", baseAssetSha256: udimBase.sha256, basePixels: opaqueBlack, offset: 12, dirty: [0, 0, 255, 255] }); client.terminate(); const restarted = new StorageClient(); const reopenedPacked = await restarted.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: packedId, tile: 1001 }); const reopenedUdim = await restarted.readTexturePaintTileBinding({ schemaVersion: 1, projectId, textureAssetId: udimId, tile: 1002 }); const reopenedAsset = await restarted.readAsset(projectId, reopenedPacked.binding!.assetSha256); const reopenedPixels = await decode(reopenedAsset.data); restarted.terminate(); return { packedBase: packedBase.sha256, packedFirst: packedFirst.binding, firstPixels: Array.from(firstPixels), injected, afterFailure: afterFailure.binding, packedSecond: packedSecond.binding, udim: udim.binding, reopenedPacked: reopenedPacked.binding, reopenedUdim: reopenedUdim.binding, reopenedPixels: Array.from(reopenedPixels), expectedPackedPixels: Array.from(packedSecondPixels), }; }); expect(result.packedFirst).toMatchObject({ kind: "PACKED", tile: 1001, generation: 1, pixelSha256: expect.stringMatching(/^[a-f0-9]{64}$/) }); expect(result.packedFirst.assetSha256).not.toBe(result.packedBase); expect(result.firstPixels.slice(0, 4)).toEqual([255, 0, 0, 255]); expect(result.injected).toContain("STORAGE_TRANSACTION"); expect(result.afterFailure).toEqual(result.packedFirst); expect(result.packedSecond).toMatchObject({ kind: "PACKED", tile: 1001, generation: 2 }); expect(result.packedSecond.assetSha256).not.toBe(result.packedFirst.assetSha256); expect(result.udim).toMatchObject({ kind: "UDIM", tile: 1002, generation: 1 }); expect(result.reopenedPacked).toEqual(result.packedSecond); expect(result.reopenedUdim).toEqual(result.udim); expect(result.reopenedPixels).toEqual(result.expectedPackedPixels); });