import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { expect, test } from "@playwright/test"; const root = path.resolve(import.meta.dirname, "../../.."); const fixturePath = path.join(root, "tests/files/web/modifier_reader_batch_scene.blend"); const fixture = fs.readFileSync(fixturePath); const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex"); const expectedTypes = ["DATA_TRANSFER", "DYNAMIC_PAINT", "EXPLODE", "FLUID"]; test("modifier reader batch preserves concrete Blender Main metadata", async ({ page }) => { test.setTimeout(180_000); await page.goto("/"); const result = await page.evaluate(async (bytes) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const canonical = (snapshot: any) => snapshot.meshes.flatMap((mesh: any) => (mesh.modifierStack ?? []).map((modifier: any) => ({ meshId: mesh.id, uuid: modifier.uuid, name: modifier.name, type: modifier.type, enabled: modifier.enabled, evaluationStatus: modifier.evaluationStatus, parameters: modifier.parameters ?? null, }))); const client = new WebEngineClient({ timeoutMs: 60_000 }); const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 }); const secondClient = new WebEngineClient({ timeoutMs: 60_000 }); const output: any = {}; try { const opened = await client.openBlend(Uint8Array.from(bytes).buffer); output.before = canonical(opened.snapshot); output.types = output.before.map((modifier: any) => modifier.type); const saved = await client.saveBlend(); output.after = canonical((await reopenedClient.openBlend(saved.slice(0))).snapshot); output.second = canonical((await secondClient.openBlend(Uint8Array.from(bytes).buffer)).snapshot); output.saveReopenExact = JSON.stringify(output.before) === JSON.stringify(output.after); output.deterministicExact = JSON.stringify(output.before) === JSON.stringify(output.second); output.savedSha256 = await crypto.subtle.digest("SHA-256", saved).then((hash) => Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("")); try { await client.openBlend(Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44]).buffer); output.malformed = { code: "ACCEPTED_MALFORMED" }; } catch (error: any) { output.malformed = { code: error?.code, message: error?.message }; } } catch (error: any) { output.error = { code: error?.code, message: error?.message, detail: String(error) }; } finally { client.terminate(); reopenedClient.terminate(); secondClient.terminate(); } return output; }, Array.from(fixture)); const report = { schemaVersion: 1, task: "WBV2-P1146-001", operation: "MODIFIER_READER_BATCH_CHROMIUM_SAVE_REOPEN", browser: "chromium", queueMutation: false, fixture: "tests/files/web/modifier_reader_batch_scene.blend", fixtureSha256, expectedTypes, sourceUnits: [ "modifier-source:MOD_datatransfer", "modifier-source:MOD_dynamicpaint", "modifier-source:MOD_explode", "modifier-source:MOD_cast", "rna-struct:DataTransferModifier", "rna-struct:DynamicPaintModifier", "rna-struct:ExplodeModifier", "rna-struct:CastModifier", ], ...result, }; fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1146-001"), { recursive: true }); fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1146-001/modifier-reader-batch-production.json"), `${JSON.stringify(report, null, 2)}\n`); expect(result.error).toBeUndefined(); expect(result.types).toEqual(expect.arrayContaining(expectedTypes)); expect(result.saveReopenExact).toBe(true); expect(result.deterministicExact).toBe(true); expect(result.malformed?.code).not.toBe("ACCEPTED_MALFORMED"); });