import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const fixture = JSON.parse(fs.readFileSync( path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v1.json"), "utf8", )) as unknown; test("M12-01G aborts catalog migration without changing the old IndexedDB index", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async (source) => { const storage = await import("/src/storage/asset-catalog-indexeddb.ts"); const open = (name: string): Promise => new Promise((resolve, reject) => { const request = indexedDB.open(name, 1); request.onupgradeneeded = () => { request.result.createObjectStore("setting", { keyPath: "id" }); request.result.createObjectStore("migration", { 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 seed = async (database: IDBDatabase): Promise => { const transaction = database.transaction(["setting", "migration"], "readwrite"); transaction.objectStore("setting").put({ id: storage.ASSET_CATALOG_INDEX_V1_ID, value: source }); transaction.objectStore("migration").put({ id: "schema-legacy", version: 1, marker: "unchanged" }); await complete(transaction); }; 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 snapshot = async (database: IDBDatabase) => { const transaction = database.transaction(["setting", "migration"], "readonly"); const settings = transaction.objectStore("setting"); const migrations = transaction.objectStore("migration"); const values = await Promise.all([ get(settings, storage.ASSET_CATALOG_INDEX_V1_ID), get(settings, storage.ASSET_CATALOG_INDEX_V2_ID), get(migrations, storage.ASSET_CATALOG_MIGRATION_ID), get(migrations, "schema-legacy"), ]); await complete(transaction); return { version: database.version, source: values[0], target: values[1], receipt: values[2], legacy: values[3] }; }; const faultCode = async (database: IDBDatabase, faultAt: "AFTER_TARGET_PUT" | "AFTER_SOURCE_DELETE"): Promise => { try { await storage.migrateAssetCatalogIndexedDB(database, { faultAt }); return ""; } catch (error) { return String((error as Error & { code?: string }).code ?? ""); } }; const databaseName = `m12-01g-${crypto.randomUUID()}`; let database = await open(databaseName); await seed(database); const baseline = await snapshot(database); const afterTargetCode = await faultCode(database, "AFTER_TARGET_PUT"); database.close(); database = await open(databaseName); const afterTargetFault = await snapshot(database); const afterDeleteCode = await faultCode(database, "AFTER_SOURCE_DELETE"); database.close(); database = await open(databaseName); const afterDeleteFault = await snapshot(database); const migrated = await storage.migrateAssetCatalogIndexedDB(database); const committed = await snapshot(database); database.close(); indexedDB.deleteDatabase(databaseName); return { baseline, afterTargetCode, afterTargetFault, afterDeleteCode, afterDeleteFault, migrated: { status: migrated.status, revision: migrated.manifest.revision, sourceHash: migrated.receipt.sourceManifestSha256, targetHash: migrated.receipt.targetManifestSha256, }, committed, }; }, fixture); expect(result.afterTargetCode).toBe("STORAGE_TRANSACTION"); expect(result.afterDeleteCode).toBe("STORAGE_TRANSACTION"); expect(result.afterTargetFault).toEqual(result.baseline); expect(result.afterDeleteFault).toEqual(result.baseline); expect(result.baseline.version).toBe(1); expect(result.baseline.source).toEqual({ id: "asset-catalog:index:v1", value: fixture }); expect(result.baseline.target).toBeUndefined(); expect(result.baseline.receipt).toBeUndefined(); expect(result.baseline.legacy).toEqual({ id: "schema-legacy", version: 1, marker: "unchanged" }); expect(result.migrated).toEqual({ status: "MIGRATED", revision: 7, sourceHash: "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881", targetHash: "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", }); expect(result.committed.version).toBe(1); expect(result.committed.source).toBeUndefined(); expect(result.committed.target).toMatchObject({ id: "asset-catalog:index:v2", value: { schemaVersion: 2, revision: 7 } }); expect(result.committed.receipt).toMatchObject({ id: "asset-catalog:migration:v1-to-v2", task: "M12-01G", status: "MIGRATED" }); expect(result.committed.legacy).toEqual(result.baseline.legacy); });