133 lines
6.6 KiB
TypeScript
133 lines
6.6 KiB
TypeScript
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<string, unknown>;
|
|
const identity = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8")) as Record<string, unknown>;
|
|
const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png"));
|
|
|
|
test("M12-02G reclaims only the final project-scoped preview payload reference", async ({ page }) => {
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => {
|
|
const commit = await import("/src/storage/asset-preview-opfs-commit.ts");
|
|
const gc = await import("/src/storage/asset-preview-reference-gc.ts");
|
|
const quarantine = await import("/src/storage/asset-preview-quarantine.ts");
|
|
const encoded = new Uint8Array(bytes).buffer;
|
|
const base = await commit.parseAssetCatalogManifestV2({
|
|
...sourceCatalog,
|
|
assets: (sourceCatalog.assets as Array<Record<string, unknown>>).map((asset) => ({ ...asset, preview: null })),
|
|
});
|
|
const identityFor = async (assetId: string) => {
|
|
const input = { ...sourceIdentity, assetId } as Record<string, unknown>;
|
|
delete input.identitySha256;
|
|
return commit.createAssetPreviewIdentity(input as never);
|
|
};
|
|
const identities = await Promise.all(base.assets.map((asset) => identityFor(asset.assetId)));
|
|
const open = (name: string): Promise<IDBDatabase> => 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<void> => new Promise((resolve, reject) => {
|
|
transaction.oncomplete = () => resolve();
|
|
transaction.onerror = () => reject(transaction.error);
|
|
transaction.onabort = () => reject(transaction.error);
|
|
});
|
|
const seed = async (database: IDBDatabase) => {
|
|
const transaction = database.transaction("setting", "readwrite");
|
|
transaction.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base });
|
|
await complete(transaction);
|
|
};
|
|
const exists = async (projectId: string, sha256: string): Promise<boolean> => {
|
|
try {
|
|
const root = await navigator.storage.getDirectory();
|
|
let current = root;
|
|
for (const segment of ["projects", projectId, "assets", "sha256", sha256.slice(0, 2)]) current = await current.getDirectoryHandle(segment);
|
|
await current.getFileHandle(sha256);
|
|
return true;
|
|
}
|
|
catch { return false; }
|
|
};
|
|
const databaseAName = `m12-02g-a-${crypto.randomUUID()}`;
|
|
const databaseBName = `m12-02g-b-${crypto.randomUUID()}`;
|
|
const projectA = `m12-02g-a-${crypto.randomUUID()}`;
|
|
const projectB = `m12-02g-b-${crypto.randomUUID()}`;
|
|
let databaseA = await open(databaseAName);
|
|
let databaseB = await open(databaseBName);
|
|
await seed(databaseA);
|
|
await seed(databaseB);
|
|
await commit.commitAssetPreviewToOPFS(databaseA, projectA, 7, identities[0], encoded.slice(0));
|
|
await commit.commitAssetPreviewToOPFS(databaseA, projectA, 8, identities[1], encoded.slice(0));
|
|
await commit.commitAssetPreviewToOPFS(databaseB, projectB, 7, identities[0], encoded.slice(0));
|
|
|
|
const firstRemoval = await gc.removeAssetPreviewReference(databaseA, projectA, 9, base.assets[0].assetId);
|
|
const afterFirstA = await exists(projectA, identities[0].content.sha256);
|
|
const afterFirstB = await exists(projectB, identities[0].content.sha256);
|
|
const finalRemoval = await gc.removeAssetPreviewReference(databaseA, projectA, 10, base.assets[1].assetId);
|
|
const afterFinalA = await exists(projectA, identities[0].content.sha256);
|
|
const afterFinalB = await exists(projectB, identities[0].content.sha256);
|
|
const projectBReady = await quarantine.inspectAssetPreviewWithQuarantine(databaseB, projectB, base.assets[0].assetId);
|
|
|
|
databaseA.close();
|
|
databaseA = await open(databaseAName);
|
|
const transaction = databaseA.transaction("setting", "readonly");
|
|
const row = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
const request = transaction.objectStore("setting").get("asset-catalog:index:v2");
|
|
request.onsuccess = () => resolve(request.result as Record<string, unknown>);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
await complete(transaction);
|
|
const reopenedA = await commit.parseAssetCatalogManifestV2(row.value);
|
|
const value = {
|
|
firstReceipt: firstRemoval.receipt,
|
|
finalReceipt: finalRemoval.receipt,
|
|
afterFirstA,
|
|
afterFirstB,
|
|
afterFinalA,
|
|
afterFinalB,
|
|
projectBReady: { status: projectBReady.status, bytes: projectBReady.status === "READY" ? projectBReady.data.byteLength : 0 },
|
|
reopenedRevision: reopenedA.revision,
|
|
reopenedPreviews: reopenedA.assets.map((asset) => asset.preview),
|
|
baseMetadata: base.assets,
|
|
reopenedMetadata: reopenedA.assets.map((asset) => ({ ...asset, preview: null })),
|
|
};
|
|
databaseA.close();
|
|
databaseB.close();
|
|
indexedDB.deleteDatabase(databaseAName);
|
|
indexedDB.deleteDatabase(databaseBName);
|
|
const root = await navigator.storage.getDirectory();
|
|
const projects = await root.getDirectoryHandle("projects");
|
|
await projects.removeEntry(projectA, { recursive: true });
|
|
await projects.removeEntry(projectB, { recursive: true });
|
|
return value;
|
|
}, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] });
|
|
|
|
expect(result.firstReceipt).toMatchObject({
|
|
task: "M12-02G",
|
|
status: "RETAINED",
|
|
baseRevision: 9,
|
|
committedRevision: 10,
|
|
remainingProjectReferences: 1,
|
|
payloadReclaimed: false,
|
|
});
|
|
expect(result.finalReceipt).toMatchObject({
|
|
task: "M12-02G",
|
|
status: "COLLECTED",
|
|
baseRevision: 10,
|
|
committedRevision: 11,
|
|
remainingProjectReferences: 0,
|
|
payloadReclaimed: true,
|
|
});
|
|
expect({
|
|
afterFirstA: result.afterFirstA,
|
|
afterFirstB: result.afterFirstB,
|
|
afterFinalA: result.afterFinalA,
|
|
afterFinalB: result.afterFinalB,
|
|
}).toEqual({ afterFirstA: true, afterFirstB: true, afterFinalA: false, afterFinalB: true });
|
|
expect(result.projectBReady).toEqual({ status: "READY", bytes: 513 });
|
|
expect(result.reopenedRevision).toBe(11);
|
|
expect(result.reopenedPreviews).toEqual([null, null]);
|
|
expect(result.reopenedMetadata).toEqual(result.baseMetadata);
|
|
});
|