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 fixtures = JSON.parse(process.env.WBV2_BATCH_FIXTURES ?? JSON.stringify(process.env.WBV2_BATCH_FIXTURE ? [process.env.WBV2_BATCH_FIXTURE] : [])) as string[]; const task = process.env.WBV2_BATCH_TASK ?? "WBV2-P1152-001"; const productionPath = process.env.WBV2_BATCH_PRODUCTION ?? `tests/golden/${task}/modifier-reader-batch-production.json`; const expectedTypes = JSON.parse(process.env.WBV2_BATCH_TYPES ?? "[]") as string[]; const sourceUnits = JSON.parse(process.env.WBV2_BATCH_UNITS ?? "[]") as string[]; if (!fixtures.length || !expectedTypes.length || !sourceUnits.length) throw new Error("multi-fixture batch requires fixtures, expected types and source units"); const fixtureBytes = Object.fromEntries(fixtures.map((name) => [name, Array.from(fs.readFileSync(path.join(root, name)))])); const fixtureSha256 = Object.fromEntries(fixtures.map((name) => [name, crypto.createHash("sha256").update(Buffer.from(fixtureBytes[name])).digest("hex")])); test("multi-fixture modifier reader batch preserves Blender Main metadata", async ({ page }) => { test.setTimeout(180_000); await page.goto("/"); const result = await page.evaluate(async (input) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const canonical = (snapshot: any) => [...(snapshot.meshes ?? []), ...(snapshot.nodes ?? []), ...(snapshot.volumes ?? []), ...(snapshot.objects ?? [])].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 output: Record = {}; for (const [name, bytes] of Object.entries(input.fixtures as Record)) { const client = new WebEngineClient({ timeoutMs: 60_000 }); const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 }); const secondClient = new WebEngineClient({ timeoutMs: 60_000 }); try { const opened = await client.openBlend(Uint8Array.from(bytes).buffer); const before = canonical(opened.snapshot); const saved = await client.saveBlend(); const after = canonical((await reopenedClient.openBlend(saved.slice(0))).snapshot); const second = canonical((await secondClient.openBlend(Uint8Array.from(bytes).buffer)).snapshot); output[name] = { before, types: before.map((modifier: any) => modifier.type), after, second, saveReopenExact: JSON.stringify(before) === JSON.stringify(after), deterministicExact: JSON.stringify(before) === JSON.stringify(second), 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[name].malformed = { code: "ACCEPTED_MALFORMED" }; } catch (error: any) { output[name].malformed = { code: error?.code, message: error?.message }; } } catch (error: any) { output[name] = { error: { code: error?.code, message: error?.message, detail: String(error) } }; } finally { client.terminate(); reopenedClient.terminate(); secondClient.terminate(); } } return output; }, { fixtures: fixtureBytes }); const report = { schemaVersion: 1, task, operation: "MODIFIER_READER_MULTI_FIXTURE_CHROMIUM_SAVE_REOPEN", browser: "chromium", queueMutation: false, fixtures, fixtureSha256, expectedTypes, sourceUnits, ...result, }; fs.mkdirSync(path.join(root, `tests/golden/${task}`), { recursive: true }); fs.mkdirSync(path.dirname(path.join(root, productionPath)), { recursive: true }); fs.writeFileSync(path.join(root, productionPath), `${JSON.stringify(report, null, 2)}\n`); const observedTypes = new Set(Object.values(result).flatMap((entry: any) => entry?.types ?? [])); for (const type of expectedTypes) expect(observedTypes.has(type)).toBe(true); for (const fixture of fixtures) { expect(result[fixture]?.error).toBeUndefined(); expect(result[fixture]?.saveReopenExact).toBe(true); expect(result[fixture]?.deterministicExact).toBe(true); expect(result[fixture]?.malformed?.code).not.toBe("ACCEPTED_MALFORMED"); } });