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 = { OBJ: fs.readFileSync(path.join(root, "tests/files/web/m12_obj_multi_v1/multi-object.obj")), STL: fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl")), PLY: fs.readFileSync(path.join(root, "tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply")), }; const reportPath = path.join(root, "tests/golden/M12-07J/io-format-recovery-report.json"); const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex"); test("M12-07J recovers OBJ/STL/PLY after cancellation, OOM budget faults and Worker restart", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async (input) => { const recovery = await import("/src/testing/io-format-recovery.ts"); const run = (format: "OBJ" | "STL" | "PLY", bytes: number[], generation: number, phase: string) => new Promise((resolve, reject) => { const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" }); const requestId = `${format.toLowerCase()}-${phase}-${generation}`; worker.onmessage = (event: MessageEvent) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); }; worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; const payload = Uint8Array.from(bytes).buffer; worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: generation, baseRevision: 4 }, [payload]); }); const cancel = (format: "OBJ" | "STL" | "PLY", bytes: number[]) => new Promise((resolve, reject) => { const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" }); const requestId = `${format.toLowerCase()}-cancel-1`; worker.onmessage = (event: MessageEvent) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); }; worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; const payload = Uint8Array.from(bytes).buffer; worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: 1, baseRevision: 4 }, [payload]); setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3); }); const summary: Record = {}; for (const format of ["OBJ", "STL", "PLY"] as const) { const source = input[format]; const cancelled = await cancel(format, source); const oversized = new Array(512 * 1024 + 1).fill(7); const oom = await run(format, oversized, 1, "oom"); const first = await run(format, source, 1, "first"); const second = await run(format, source, 2, "second"); const recovered = recovery.recoverIOFormatRecoveryOperation(first.receipt, 2); const small = await run(format, source, 3, "small"); summary[format] = { sourceSha256: await crypto.subtle.digest("SHA-256", Uint8Array.from(source)).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")), cancel: cancelled.receipt, oom: oom.receipt, first: first.receipt, second: second.receipt, recovered, small: small.receipt, hashes: { first: first.result.outputSha256, second: second.result.outputSha256, small: small.result.outputSha256 } }; } return summary; }, Object.fromEntries(Object.entries(fixtures).map(([format, bytes]) => [format, Array.from(bytes)]))); for (const format of ["OBJ", "STL", "PLY"] as const) { const value = result[format]; expect(value.cancel).toMatchObject({ status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false }); expect(value.oom).toMatchObject({ status: "BLOCKED", errorCode: "IO_FORMAT_OOM", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false }); expect(value.recovered).toMatchObject({ status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED", committed: true }); expect(value.first).toMatchObject({ status: "COMMITTED", workerGeneration: 1, publishedResults: 1, committed: true }); expect(value.second).toMatchObject({ status: "COMMITTED", workerGeneration: 2, publishedResults: 1, committed: true }); expect(value.small).toMatchObject({ status: "COMMITTED", workerGeneration: 3, publishedResults: 1, committed: true }); expect(value.hashes.second).toBe(value.hashes.first); expect(value.hashes.small).toBe(value.hashes.first); } const report = { schemaVersion: 1, task: "M12-07J", operation: "IO_FORMAT_THREE_WAY_RECOVERY", formats: result, assertions: { formats: ["OBJ", "STL", "PLY"], cancellationUnpublished: true, oomUnpublished: true, restartHashStable: true, smallRecoveryStable: true }, nextTask: "M13-01A" }; if (process.env.UPDATE_IO_FORMAT_RECOVERY_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); });