import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const catalog = JSON.parse(fs.readFileSync( path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), "utf8", )) as Record; const identity = JSON.parse(fs.readFileSync( path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8", )) as Record; const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); test("M12-02D publishes a catalog preview only after verified OPFS persistence", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { const storage = await import("/src/storage/asset-preview-opfs-commit.ts"); const encoded = new Uint8Array(bytes).buffer; const base = await storage.parseAssetCatalogManifestV2({ ...sourceCatalog, assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), }); const targetAssetId = base.assets[0].assetId; const identityInput = { ...sourceIdentity, assetId: targetAssetId } as Record; delete identityInput.identitySha256; const boundIdentity = await storage.createAssetPreviewIdentity(identityInput as never); const open = (name: string): Promise => new Promise((resolve, reject) => { const request = indexedDB.open(name, 1); request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }); const get = (store: IDBObjectStore, id: string): Promise => new Promise((resolve, reject) => { const request = store.get(id); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const seed = async (database: IDBDatabase): Promise => { const transaction = database.transaction("setting", "readwrite"); transaction.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); await complete(transaction); }; const snapshot = async (database: IDBDatabase) => { const transaction = database.transaction("setting", "readonly"); const store = transaction.objectStore("setting"); const [index, receipt] = await Promise.all([ get(store, "asset-catalog:index:v2"), get(store, storage.ASSET_PREVIEW_CATALOG_HEAD_ID), ]); await complete(transaction); return { index, receipt }; }; const payloadExists = async (projectId: string): Promise => { try { const root = await navigator.storage.getDirectory(); let current = root; for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) { current = await current.getDirectoryHandle(segment); } const file = await (await current.getFileHandle(boundIdentity.content.sha256)).getFile(); return file.size === encoded.byteLength; } catch { return false; } }; const cleanupProject = async (projectId: string): Promise => { const root = await navigator.storage.getDirectory(); try { const projects = await root.getDirectoryHandle("projects"); await projects.removeEntry(projectId, { recursive: true }); } catch { /* The before-write fault creates no project directory. */ } }; const fault = async (faultAt: "BEFORE_OPFS_WRITE" | "AFTER_OPFS_WRITE" | "AFTER_CATALOG_PUT") => { const databaseName = `m12-02d-${faultAt.toLowerCase()}-${crypto.randomUUID()}`; const projectId = `m12-02d-${crypto.randomUUID()}`; let database = await open(databaseName); await seed(database); let code = ""; try { await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0), { faultAt }); } catch (error) { code = String((error as Error & { code?: string }).code ?? ""); } database.close(); database = await open(databaseName); const state = await snapshot(database); const persisted = await payloadExists(projectId); database.close(); indexedDB.deleteDatabase(databaseName); await cleanupProject(projectId); return { faultAt, code, state, persisted }; }; const faults = []; for (const faultAt of ["BEFORE_OPFS_WRITE", "AFTER_OPFS_WRITE", "AFTER_CATALOG_PUT"] as const) { faults.push(await fault(faultAt)); } const databaseName = `m12-02d-success-${crypto.randomUUID()}`; const projectId = `m12-02d-${crypto.randomUUID()}`; let database = await open(databaseName); await seed(database); const committed = await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0)); database.close(); database = await open(databaseName); const reopened = await storage.loadCommittedAssetPreviewCatalog(database); const successState = await snapshot(database); const persisted = await payloadExists(projectId); database.close(); indexedDB.deleteDatabase(databaseName); await cleanupProject(projectId); return { base, targetAssetId, identitySha256: boundIdentity.identitySha256, contentSha256: boundIdentity.content.sha256, faults, committed: { revision: committed.manifest.revision, preview: committed.preview, receipt: committed.receipt, deduplicated: committed.deduplicated, }, reopened: { revision: reopened.manifest.revision, preview: reopened.preview, receipt: reopened.receipt, }, successState, persisted, }; }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); for (const failure of result.faults) { expect(failure.code).toBe("STORAGE_TRANSACTION"); expect(failure.state.index).toEqual({ id: "asset-catalog:index:v2", value: result.base }); expect(failure.state.receipt).toBeUndefined(); } expect(result.faults.map((failure) => failure.persisted)).toEqual([false, true, true]); expect(result.persisted).toBe(true); expect(result.committed.revision).toBe(8); expect(result.committed.deduplicated).toBe(false); expect(result.committed.preview).toEqual({ sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513, }); expect(result.committed.receipt).toMatchObject({ task: "M12-02D", status: "COMMITTED", assetId: result.targetAssetId, baseRevision: 7, committedRevision: 8, previewIdentitySha256: result.identitySha256, contentSha256: result.contentSha256, }); expect(result.reopened).toEqual({ revision: result.committed.revision, preview: result.committed.preview, receipt: result.committed.receipt, }); expect(result.successState.receipt).toEqual(result.committed.receipt); });