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 task = process.env.WBV2_BATCH_TASK ?? "WBV2-P1147-001"; const fixtureName = process.env.WBV2_BATCH_FIXTURE ?? "tests/files/web/modifier_reader_batch_two_scene.blend"; const fixturePath = path.join(root, fixtureName); const fixture = fs.readFileSync(fixturePath); const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex"); const expectedTypes = JSON.parse(process.env.WBV2_BATCH_TYPES ?? '["LAPLACIANDEFORM","LAPLACIANSMOOTH","MASK","SHRINKWRAP"]'); const sourceUnits = JSON.parse(process.env.WBV2_BATCH_UNITS ?? '["modifier-source:MOD_laplaciandeform","modifier-source:MOD_laplaciansmooth","modifier-source:MOD_mask","modifier-source:MOD_shrinkwrap","rna-struct:LaplacianDeformModifier","rna-struct:LaplacianSmoothModifier","rna-struct:MaskModifier","rna-struct:ShrinkwrapModifier"]'); test("second 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 ?? []), ...(snapshot.nodes ?? [])].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, operation: "MODIFIER_READER_BATCH_TWO_CHROMIUM_SAVE_REOPEN", browser: "chromium", queueMutation: false, fixture: fixtureName, fixtureSha256, expectedTypes, sourceUnits, ...result, }; fs.mkdirSync(path.join(root, `tests/golden/${task}`), { recursive: true }); fs.writeFileSync(path.join(root, `tests/golden/${task}/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"); });