Advance Blender 5.2 web parity through M12-03D
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-17 17:30:27 -04:00
parent 0fe8d2bb56
commit 5a11045ca5
148 changed files with 11683 additions and 66 deletions

View File

@@ -0,0 +1,114 @@
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<IDBDatabase> => 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<void> => new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
const seed = async (database: IDBDatabase): Promise<void> => {
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<unknown> => 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<string> => {
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);
});

View File

@@ -0,0 +1,91 @@
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-01H preserves catalog order and asset identity across page and Worker restarts", async ({ page }) => {
await page.goto("/");
const databaseName = `m12-01h-${Date.now()}`;
const baseline = await page.evaluate(async ({ databaseName, source }) => {
const storage = await import("/src/storage/asset-catalog-indexeddb.ts");
const database = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(databaseName, 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 transaction = database.transaction("setting", "readwrite");
transaction.objectStore("setting").put({ id: storage.ASSET_CATALOG_INDEX_V1_ID, value: source });
await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
const snapshot = await storage.loadAssetCatalogIndexedDBSnapshot(database);
database.close();
return snapshot;
}, { databaseName, source: fixture });
await page.reload();
const pageRestart = await page.evaluate(async (name) => {
const storage = await import("/src/storage/asset-catalog-indexeddb.ts");
const database = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const snapshot = await storage.loadAssetCatalogIndexedDBSnapshot(database);
database.close();
return snapshot;
}, databaseName);
const workerSnapshot = async () => page.evaluate((name) => new Promise<unknown>((resolve, reject) => {
const worker = new Worker("/src/workers/asset-catalog-restart-test.worker.ts", { type: "module" });
worker.onmessage = (event) => {
worker.terminate();
event.data.ok ? resolve(event.data.snapshot) : reject(new Error(event.data.error));
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ databaseName: name });
}), databaseName);
const firstWorker = await workerSnapshot();
const secondWorker = await workerSnapshot();
expect(pageRestart).toEqual(baseline);
expect(firstWorker).toEqual(baseline);
expect(secondWorker).toEqual(baseline);
expect(baseline).toEqual({
schemaVersion: 1,
revision: 7,
manifestSha256: "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90",
catalogOrder: [
{ catalogId: "44444444-4444-4444-8444-444444444444", path: "Animation" },
{ catalogId: "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", path: "Characters" },
{ catalogId: "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", path: "Characters/Heroes" },
],
assetOrder: [
"asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
"asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
],
assetIdentities: [
{
assetId: "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d",
assetLibraryIdentifier: null,
relativeAssetIdentifier: "Action/Legacy Walk",
},
{
assetId: "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326",
assetLibraryIdentifier: null,
relativeAssetIdentifier: "Object/Legacy Hero",
},
],
});
await page.evaluate((name) => indexedDB.deleteDatabase(name), databaseName);
});

View File

@@ -0,0 +1,113 @@
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-02E deduplicates preview content without merging asset metadata", 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<Record<string, unknown>>).map((asset) => ({ ...asset, preview: null })),
});
const identities = [];
for (const asset of base.assets) {
const input = { ...sourceIdentity, assetId: asset.assetId } as Record<string, unknown>;
delete input.identitySha256;
identities.push(await storage.createAssetPreviewIdentity(input as never));
}
const databaseName = `m12-02e-${crypto.randomUUID()}`;
const projectId = `m12-02e-${crypto.randomUUID()}`;
const open = (): Promise<IDBDatabase> => new Promise((resolve, reject) => {
const request = indexedDB.open(databaseName, 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);
});
let database = await open();
const seed = database.transaction("setting", "readwrite");
seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base });
await complete(seed);
const first = await storage.commitAssetPreviewToOPFS(database, projectId, 7, identities[0], encoded.slice(0));
const second = await storage.commitAssetPreviewToOPFS(database, projectId, 8, identities[1], encoded.slice(0));
database.close();
database = await open();
const reopened = await storage.loadCommittedAssetPreviewCatalog(database);
const root = await navigator.storage.getDirectory();
let contentDirectory = root;
for (const segment of ["projects", projectId, "assets", "sha256", identities[0].content.sha256.slice(0, 2)]) {
contentDirectory = await contentDirectory.getDirectoryHandle(segment);
}
const files: string[] = [];
for await (const [name, handle] of (contentDirectory as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }).entries()) {
if (handle.kind === "file") files.push(name);
}
files.sort();
const stored = await (await contentDirectory.getFileHandle(identities[0].content.sha256)).getFile();
const storedBytes = await stored.arrayBuffer();
const storedSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", storedBytes)),
(byte) => byte.toString(16).padStart(2, "0")).join("");
const withoutPreview = (asset: Record<string, unknown>) => {
const copy = { ...asset };
delete copy.preview;
return copy;
};
const resultValue = {
first: { revision: first.manifest.revision, deduplicated: first.deduplicated, identity: identities[0].identitySha256 },
second: { revision: second.manifest.revision, deduplicated: second.deduplicated, identity: identities[1].identitySha256 },
contentSha256: identities[0].content.sha256,
files,
storedBytes: stored.size,
storedSha256,
baseMetadata: base.assets.map((asset) => withoutPreview(asset as unknown as Record<string, unknown>)),
committedMetadata: second.manifest.assets.map((asset) => withoutPreview(asset as unknown as Record<string, unknown>)),
previews: second.manifest.assets.map((asset) => asset.preview),
reopened: {
revision: reopened.manifest.revision,
receiptAssetId: reopened.receipt.assetId,
preview: reopened.preview,
},
assetIds: base.assets.map((asset) => asset.assetId),
};
database.close();
indexedDB.deleteDatabase(databaseName);
const projects = await root.getDirectoryHandle("projects");
await projects.removeEntry(projectId, { recursive: true });
return resultValue;
}, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] });
expect(result.first).toMatchObject({ revision: 8, deduplicated: false });
expect(result.second).toMatchObject({ revision: 9, deduplicated: true });
expect(result.first.identity).not.toBe(result.second.identity);
expect(result.files).toEqual([result.contentSha256]);
expect(result.storedBytes).toBe(513);
expect(result.storedSha256).toBe(result.contentSha256);
expect(result.committedMetadata).toEqual(result.baseMetadata);
expect(result.committedMetadata[0]).not.toEqual(result.committedMetadata[1]);
expect(result.previews).toEqual([
{ sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 },
{ sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 },
]);
expect(result.reopened).toEqual({
revision: 9,
receiptAssetId: result.assetIds[1],
preview: result.previews[1],
});
});

View File

@@ -0,0 +1,95 @@
import { expect, test } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(import.meta.dirname, "../../..");
const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8"));
const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8"));
const preview = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png"));
test("M12-02H main-thread and Offscreen preview displays match the Blender RGBA8 metrics", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async ({ identityValue, bytes, desktopValue, thresholds }) => {
const { displayAssetPreview } = await import("/src/assets/AssetPreviewDisplay.ts");
const { compareRenderImages } = await import("/src/three-adapter/render-image-comparison.ts");
document.body.replaceChildren();
document.body.style.margin = "0";
const canvas = document.createElement("canvas");
canvas.id = "m12-asset-preview";
canvas.setAttribute("aria-label", "Asset preview");
canvas.style.width = "128px";
canvas.style.height = "128px";
canvas.style.imageRendering = "pixelated";
document.body.append(canvas);
const encoded = new Uint8Array(bytes).buffer;
const main = await displayAssetPreview(canvas, "MAIN_THREAD_CANVAS_2D", identityValue, encoded.slice(0));
const workerResult = await new Promise<{ receipt: unknown; pixels: Uint8Array }>((resolve, reject) => {
const worker = new Worker("/src/workers/asset-preview-display-test.worker.ts", { type: "module" });
worker.onmessage = (event) => {
worker.terminate();
event.data.ok ? resolve(event.data) : reject(new Error(event.data.error));
};
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
worker.postMessage({ identity: identityValue, bytes: encoded.slice(0) });
});
const reference = new Uint8Array(desktopValue.pixelCount * 4);
for (let offset = 0; offset < reference.byteLength; offset += 4) reference.set(desktopValue.referencePixel, offset);
const mainMetrics = compareRenderImages(reference, main.pixels, desktopValue.width, desktopValue.height, thresholds);
const offscreenPixels = new Uint8Array(workerResult.pixels);
const offscreenMetrics = compareRenderImages(reference, offscreenPixels, desktopValue.width, desktopValue.height, thresholds);
const wrong = new Uint8Array(main.pixels);
wrong[0] ^= 0xff;
const wrongMetrics = compareRenderImages(reference, wrong, desktopValue.width, desktopValue.height, thresholds);
return {
main: { receipt: main.receipt, pixels: Array.from(main.pixels) },
offscreen: { receipt: workerResult.receipt, pixels: Array.from(offscreenPixels) },
mainMetrics,
offscreenMetrics,
wrongMetrics,
};
}, { identityValue: identity, bytes: [...preview], desktopValue: desktop, thresholds: manifest.thresholds });
const canvas = page.locator("#m12-asset-preview");
await expect(canvas).toBeVisible();
await expect(canvas).toHaveAttribute("width", "8");
await expect(canvas).toHaveAttribute("height", "8");
await expect(canvas).toHaveAttribute("data-asset-preview-status", "ready");
await expect(canvas).toHaveAttribute("data-asset-preview-backend", "MAIN_THREAD_CANVAS_2D");
await expect(canvas).toHaveCSS("width", "128px");
await expect(canvas).toHaveCSS("height", "128px");
expect(result.main.receipt).toMatchObject({
status: "READY",
backend: "MAIN_THREAD_CANVAS_2D",
width: 8,
height: 8,
pixelByteLength: 256,
pixelSha256: desktop.rgbaSha256,
nonTransparentPixels: 64,
bitmapClosed: true,
});
expect(result.offscreen.receipt).toMatchObject({
status: "READY",
backend: "OFFSCREEN_CANVAS_2D",
width: 8,
height: 8,
pixelByteLength: 256,
pixelSha256: desktop.rgbaSha256,
nonTransparentPixels: 64,
bitmapClosed: true,
});
expect(result.main.pixels).toEqual(result.offscreen.pixels);
for (const metrics of [result.mainMetrics, result.offscreenMetrics]) {
expect(metrics.status).toBe("READY");
expect(metrics.errorCode).toBeNull();
expect(metrics.meanAbsoluteError).toBe(0);
expect(metrics.rootMeanSquaredError).toBe(0);
expect(metrics.p95ChannelError).toBe(0);
expect(metrics.maxChannelError).toBe(0);
expect(metrics.badPixelRatio).toBe(0);
expect(metrics.foregroundIntersectionOverUnion).toBe(1);
expect(metrics.alphaCoverageDeltaRatio).toBe(0);
}
expect(result.wrongMetrics.status).toBe("BLOCKED");
expect(result.wrongMetrics.errorCode).toBe("RENDER_REFERENCE_MISMATCH");
});

View File

@@ -0,0 +1,171 @@
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-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<Record<string, unknown>>).map((asset) => ({ ...asset, preview: null })),
});
const targetAssetId = base.assets[0].assetId;
const identityInput = { ...sourceIdentity, assetId: targetAssetId } as Record<string, unknown>;
delete identityInput.identitySha256;
const boundIdentity = await storage.createAssetPreviewIdentity(identityInput as never);
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 get = (store: IDBObjectStore, id: string): Promise<unknown> => 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<void> => {
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<boolean> => {
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<void> => {
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);
});

View File

@@ -0,0 +1,118 @@
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-02F quarantines corrupt preview bytes while preserving catalog metadata", 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 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 target = base.assets[0];
const identityInput = { ...sourceIdentity, assetId: target.assetId } as Record<string, unknown>;
delete identityInput.identitySha256;
const boundIdentity = await commit.createAssetPreviewIdentity(identityInput as never);
const databaseName = `m12-02f-${crypto.randomUUID()}`;
const projectId = `m12-02f-${crypto.randomUUID()}`;
const open = (): Promise<IDBDatabase> => new Promise((resolve, reject) => {
const request = indexedDB.open(databaseName, 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);
});
let database = await open();
const seed = database.transaction("setting", "readwrite");
seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base });
await complete(seed);
const committed = await commit.commitAssetPreviewToOPFS(database, projectId, 7, boundIdentity, encoded.slice(0));
const ready = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId);
const root = await navigator.storage.getDirectory();
let contentDirectory = root;
for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) {
contentDirectory = await contentDirectory.getDirectoryHandle(segment);
}
const fileHandle = await contentDirectory.getFileHandle(boundIdentity.content.sha256);
const corrupt = new Uint8Array(await (await fileHandle.getFile()).arrayBuffer());
corrupt[corrupt.byteLength - 1] ^= 0xff;
const writer = await fileHandle.createWritable();
await writer.write(corrupt);
await writer.close();
const quarantined = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId);
let contentExists = true;
try { await contentDirectory.getFileHandle(boundIdentity.content.sha256); }
catch { contentExists = false; }
database.close();
database = await open();
const reopened = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId);
const catalogTransaction = database.transaction("setting", "readonly");
const catalogRow = await new Promise<Record<string, unknown>>((resolve, reject) => {
const request = catalogTransaction.objectStore("setting").get("asset-catalog:index:v2");
request.onsuccess = () => resolve(request.result as Record<string, unknown>);
request.onerror = () => reject(request.error);
});
await complete(catalogTransaction);
const storedCatalog = await commit.parseAssetCatalogManifestV2(catalogRow.value);
const quarantinePath = quarantined.status === "QUARANTINED" ? quarantined.receipt.quarantinePath : null;
let quarantineFileBytes = 0;
if (quarantinePath) {
let current = root;
const segments = quarantinePath.split("/");
for (const segment of segments.slice(0, -1)) current = await current.getDirectoryHandle(segment);
quarantineFileBytes = (await (await current.getFileHandle(segments.at(-1)!)).getFile()).size;
}
const value = {
baseMetadata: target,
ready: { status: ready.status, bytes: ready.status === "READY" ? ready.data.byteLength : 0 },
quarantined,
reopened,
contentExists,
quarantineFileBytes,
storedAsset: storedCatalog.assets[0],
storedRevision: storedCatalog.revision,
committedRevision: committed.manifest.revision,
otherAsset: storedCatalog.assets[1],
baseOtherAsset: base.assets[1],
};
database.close();
indexedDB.deleteDatabase(databaseName);
const projects = await root.getDirectoryHandle("projects");
await projects.removeEntry(projectId, { recursive: true });
return value;
}, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] });
expect(result.ready).toEqual({ status: "READY", bytes: 513 });
expect(result.quarantined.status).toBe("QUARANTINED");
expect(result.quarantined.preview).toBeNull();
expect(result.quarantined.data).toBeNull();
expect(result.quarantined.receipt).toMatchObject({
task: "M12-02F",
status: "QUARANTINED",
catalogRevision: 8,
actualByteLength: 513,
code: "ASSET_SOURCE_HASH_MISMATCH",
});
expect(result.quarantined.receipt.actualSha256).not.toBe(result.quarantined.receipt.expected.sha256);
expect(result.quarantined.receipt.quarantinePath).toContain("/assets/quarantine/");
expect(result.reopened).toEqual(result.quarantined);
expect(result.contentExists).toBe(false);
expect(result.quarantineFileBytes).toBe(513);
expect(result.storedRevision).toBe(result.committedRevision);
expect(result.storedAsset).toEqual(result.quarantined.asset);
expect({ ...result.storedAsset, preview: null }).toEqual(result.baseMetadata);
expect(result.otherAsset).toEqual(result.baseOtherAsset);
});

View File

@@ -0,0 +1,132 @@
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);
});

View File

@@ -0,0 +1,139 @@
import { expect, test } from "@playwright/test";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { computeLibraryAppendClosureSha256, LIBRARY_MAIN_APPEND_SCHEMA } from "../../protocol/library-main-append";
import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPERATION_IDENTITY_SCHEMA } from "../../protocol/library-operation-identity";
const root = path.resolve(import.meta.dirname, "../../..");
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend"));
const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend"));
test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => {
test.setTimeout(120_000);
await page.goto("/");
const closure = {
object: "Object/M12 Append Object",
mesh: "Mesh/M12 Append Mesh",
material: "Material/M12 Append Material",
image: "Image/M12 Append Image",
} as const;
const sourceSha256 = crypto.createHash("sha256").update(source).digest("hex");
const sourceIdentity = await createLibrarySourceIdentity({
sourceLocator: "project-assets/libraries/m12_append_source.blend",
sourceSha256,
});
const dependencyClosureSha256 = await computeLibraryAppendClosureSha256(closure);
const binding = await createLibraryOperationBinding({
schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA,
operation: "APPEND",
source: sourceIdentity,
sourceDataBlockId: closure.object,
owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03d", localDataBlockId: closure.object },
readOnly: false,
referenceReadOnly: false,
sourceGeneration: 1,
sourceRevision: 0,
dependencyClosureSha256,
});
const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const sourceBuffer = Uint8Array.from(sourceBytes).buffer;
const targetBuffer = Uint8Array.from(targetBytes).buffer;
const digest = async (bytes: ArrayBuffer): Promise<string> => {
const hash = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("");
};
const makeRequest = (baseRevision: number) => ({
schemaVersion: 1 as const,
baseRevision,
binding,
expectedClosure: closure,
});
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopened = new WebEngineClient({ timeoutMs: 60_000 });
const ids = {
object: "object:M12 Append Object",
mesh: "mesh:M12 Append Mesh",
material: "material:M12 Append Material",
image: "image:M12 Append Image",
};
const closureState = (snapshot: any) => ({
object: snapshot.nodes.find((item: any) => item.id === ids.object),
mesh: snapshot.meshes.find((item: any) => item.id === ids.mesh),
material: snapshot.materials.find((item: any) => item.id === ids.material),
image: snapshot.images.find((item: any) => item.id === ids.image),
});
try {
const opened = await client.openBlend(targetBuffer.slice(0));
const baseRevision = opened.snapshot.revision;
const request = await makeRequest(baseRevision);
const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request);
const appendedClosure = closureState(appended.snapshot);
const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision))
.then(() => ({ code: "NO_ERROR" }))
.catch((error: any) => ({ code: error.code, message: error.message }));
const collisionRequest = await makeRequest(appended.snapshot.revision);
const collision = await client.appendLibraryObject(sourceBuffer.slice(0), collisionRequest)
.then(() => ({ code: "NO_ERROR" }))
.catch((error: any) => ({ code: error.code, message: error.message }));
const afterCollision = await client.snapshot();
const undone = await client.applyCommand({ type: "undo" });
const redone = await client.applyCommand({ type: "redo" });
const saved = await client.saveBlend();
const reopenedResult = await reopened.openBlend(saved);
const reopenedClosure = closureState(reopenedResult.snapshot);
return {
sourceSha256: await digest(sourceBuffer),
baseRevision,
appendedRevision: appended.snapshot.revision,
receipt: appended.receipt,
delta: appended.delta,
appendedClosure: {
object: appendedClosure.object && { type: appendedClosure.object.type, dataId: appendedClosure.object.dataId },
mesh: appendedClosure.mesh && { vertexCount: appendedClosure.mesh.vertexCount, materialSlotIds: appendedClosure.mesh.materialSlotIds },
material: appendedClosure.material && {
imageIds: appendedClosure.material.imageIds ?? appendedClosure.material.nodes?.flatMap((node: any) => node.imageId ? [node.imageId] : []),
},
image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height },
},
stale,
collision,
collisionRevision: afterCollision.snapshot.revision,
undoHasObject: Boolean(closureState(undone.snapshot).object),
redoHasObject: Boolean(closureState(redone.snapshot).object),
reopenedRevision: reopenedResult.snapshot.revision,
reopenedHasObject: Boolean(reopenedClosure.object),
reopenedHasLocalImage: reopenedClosure.image?.libraryLinked === false,
};
}
catch (error: any) {
return { error: { code: error?.code, message: error?.message, detail: error?.detail } };
}
finally {
client.terminate();
reopened.terminate();
}
}, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding });
expect(result.error).toBeUndefined();
expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/);
expect(result.appendedRevision).toBe(result.baseRevision + 1);
expect(result.receipt).toMatchObject({ operation: "APPEND", transactionCount: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision });
expect(result.receipt.mapping).toHaveLength(4);
expect(result.receipt.mapping.every((item: any) => item.owner === "LOCAL_MAIN" && item.readOnly === false && item.source === item.local)).toBe(true);
expect(result.delta).toMatchObject({ schemaVersion: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision });
expect(result.appendedClosure).toEqual({
object: { type: "MESH", dataId: "mesh:M12 Append Mesh" },
mesh: { vertexCount: 4, materialSlotIds: ["material:M12 Append Material"] },
material: { imageIds: ["image:M12 Append Image"] },
image: { libraryLinked: false, width: 2, height: 2 },
});
expect(result.stale.code).toBe("REVISION_CONFLICT");
expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID");
expect(result.collisionRevision).toBe(result.appendedRevision);
expect(result.undoHasObject).toBe(false);
expect(result.redoHasObject).toBe(true);
expect(result.reopenedHasObject).toBe(true);
expect(result.reopenedHasLocalImage).toBe(true);
});

View File

@@ -0,0 +1,72 @@
import { expect, test } from "@playwright/test";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-14/render-compositor-media-recovery.json"), "utf8")) as {
domains: Array<{
domain: "RENDER" | "COMPOSITOR" | "MEDIA";
fixture: string;
sha256: string;
cancellationCode: string;
budgetCode: string;
}>;
textureFixture: { path: string; sha256: string };
};
const fixtures = Object.fromEntries(manifest.domains.map((item) => [item.domain, fs.readFileSync(path.join(root, item.fixture))]));
const texture = fs.readFileSync(path.join(root, manifest.textureFixture.path));
const sha256 = (value: Buffer) => crypto.createHash("sha256").update(value).digest("hex");
test("M11-14 recovers real render, compositor and media production paths after lifecycle faults", async ({ page }) => {
test.setTimeout(120_000);
for (const item of manifest.domains) expect(sha256(fixtures[item.domain])).toBe(item.sha256);
expect(sha256(texture)).toBe(manifest.textureFixture.sha256);
await page.goto("/");
const reports = await page.evaluate(async ({ renderBlend, compositorBlend, movie, texturePng }) => {
const { runRenderCompositorMediaRecoverySuite } = await import("/src/testing/render-compositor-media-recovery.ts");
return runRenderCompositorMediaRecoverySuite({
renderBlend: Uint8Array.from(renderBlend).buffer,
compositorBlend: Uint8Array.from(compositorBlend).buffer,
movie: { mimeType: "video/mp4", data: Uint8Array.from(movie).buffer },
texturePng: Uint8Array.from(texturePng).buffer,
});
}, {
renderBlend: Array.from(fixtures.RENDER),
compositorBlend: Array.from(fixtures.COMPOSITOR),
movie: Array.from(fixtures.MEDIA),
texturePng: Array.from(texture),
});
expect(reports.map((report) => report.domain)).toEqual(["RENDER", "COMPOSITOR", "MEDIA"]);
for (const [index, report] of reports.entries()) {
const expected = manifest.domains[index];
expect(report.source.sha256).toBe(expected.sha256);
expect(report.cancellation).toEqual({
status: "CANCELLED",
code: expected.cancellationCode,
publishedResults: 0,
temporaryResourcesAfter: 0,
});
expect(report.restart).toMatchObject({ status: "RECOVERED", generationBefore: 1, generationAfter: 2 });
expect(report.restart.identityAfter).toBe(report.restart.identityBefore);
expect(report.restart.outputSha256After).toBe(report.restart.outputSha256Before);
expect(report.budget).toEqual({
status: "BLOCKED",
code: expected.budgetCode,
retainedIdentityHash: report.restart.identityAfter,
temporaryResourcesAfter: 0,
});
expect(report.release.status).toBe("RELEASED");
expect(report.release.releasedBytes).toBeGreaterThan(0);
expect(report.release.releasedResources).toBeGreaterThan(0);
expect(report.release.resourcesAfter).toBe(0);
expect(report.recovery).toMatchObject({
status: "RECOVERED",
identityHash: report.restart.identityAfter,
outputSha256: report.restart.outputSha256After,
});
expect(report.recovery.outputBytes).toBeGreaterThan(0);
expect(report.recovery.visibleUnits).toBeGreaterThan(0);
}
});

View File

@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-compatibility-unit-"));
const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-compatibility.ts"];
for (const sourceName of sources) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"'));
}
const compatibility = await import(pathToFileURL(path.join(temporary, "asset-catalog-compatibility.mjs")));
const v1 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8"));
const v2 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8"));
const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01E/compatibility-report.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01E/manifest.json"), "utf8"));
test("M12-01E binds the compatibility policy and generated reports", () => {
assert.equal(manifest.nextTask, "M12-01F");
for (const artifact of Object.values(manifest.artifacts)) {
const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex");
assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`);
}
});
test("M12-01E exposes schema v2 to a v1 reader as a bounded read-only snapshot", async () => {
const before = structuredClone(v2);
const report = await compatibility.inspectAssetCatalogForLegacyReader(v2, "READ");
assert.deepEqual(report, golden.read);
assert.equal(report.status, "READ_ONLY");
assert.equal(report.code, "ASSET_SCHEMA_DOWNGRADE_BLOCKED");
assert.deepEqual(report.snapshot.catalogs.map((item) => item.path), ["Animation", "Characters", "Characters/Heroes"]);
assert.deepEqual(v2, before);
});
test("M12-01E blocks every legacy write and save without changing the source hash", async () => {
for (const [operation, key] of [["CATALOG_WRITE", "catalogWrite"], ["ASSET_WRITE", "assetWrite"], ["SAVE", "save"]]) {
const report = await compatibility.inspectAssetCatalogForLegacyReader(v2, operation);
assert.deepEqual(report, golden[key]);
assert.equal(report.status, "BLOCKED");
assert.equal(report.code, "ASSET_SCHEMA_DOWNGRADE_BLOCKED");
assert.equal(report.sourceSha256, golden.read.sourceSha256);
}
});
test("M12-01E keeps native v1 ready and blocks unknown future schemas", async () => {
assert.equal((await compatibility.inspectAssetCatalogForLegacyReader(v1, "SAVE")).status, "READY");
assert.deepEqual(await compatibility.inspectAssetCatalogForLegacyReader({ schemaVersion: 3 }, "READ"), golden.future);
assert.equal(golden.future.code, "PROTOCOL_MISMATCH");
assert.equal(golden.future.recoverable, false);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-01G/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-01G binds the production transaction, browser suite, and migration inputs", () => {
assert.equal(manifest.task, "M12-01G");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-01H");
assert.deepEqual(manifest.transaction.faultPoints, ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"]);
assert.equal(manifest.transaction.failureCode, "STORAGE_TRANSACTION");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-migration-unit-"));
const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"];
for (const sourceName of sources) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
const output = result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"');
fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), output);
}
const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs")));
const source = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8"));
const target = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8"));
const report = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/migration-report.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/manifest.json"), "utf8"));
test("M12-01D binds migration inputs, outputs, and production protocols", () => {
assert.equal(manifest.nextTask, "M12-01E");
for (const artifact of Object.values(manifest.artifacts)) {
const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex");
assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`);
}
});
test("M12-01D migrates schema v1 to v2 without dropping source, preview, or library data", async () => {
const actual = await migration.migrateAssetCatalogV1ToV2(source);
assert.deepEqual(actual.manifest, target);
assert.deepEqual(actual.report, report);
assert.equal(actual.manifest.revision, 7);
assert.deepEqual(actual.manifest.catalogs.map((item) => item.path), ["Animation", "Characters", "Characters/Heroes"]);
assert.equal(actual.manifest.assets.filter((item) => item.preview !== null).length, 1);
assert.deepEqual(actual.manifest.libraries.map((item) => item.libraryId), ["library:characters", "library:materials"]);
assert.equal(actual.report.preserved.sourceBindings, 2);
});
test("M12-01D preserves canonical UUIDs and deterministically maps legacy IDs", async () => {
const first = await migration.migrateAssetCatalogV1ToV2(source);
const second = await migration.migrateAssetCatalogV1ToV2(structuredClone(source));
assert.deepEqual(second, first);
const canonical = first.report.catalogMappings.find((item) => item.legacyId.startsWith("4444"));
assert.equal(canonical.catalogId, canonical.legacyId);
assert.match(first.report.catalogMappings.find((item) => item.legacyId === "catalog:root").catalogId, /^[a-f0-9-]{36}$/);
assert.equal(first.report.sourceManifestSha256, report.sourceManifestSha256);
assert.equal(first.report.targetManifestSha256, report.targetManifestSha256);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-negatives-unit-"));
const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"];
for (const sourceName of sources) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"'));
}
const v2Protocol = await import(pathToFileURL(path.join(temporary, "asset-catalog-v2.mjs")));
const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs")));
const v1 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8"));
const v2 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8"));
const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01F/negative-cases.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01F/manifest.json"), "utf8"));
test("M12-01F binds every negative input contract", () => {
assert.equal(manifest.negativeCaseCount, golden.cases.length);
assert.equal(manifest.nextTask, "M12-01G");
for (const artifact of Object.values(manifest.artifacts)) {
const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex");
assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`);
}
});
async function codeFrom(run) {
try {
await run();
return "NO_ERROR";
}
catch (error) {
return error?.code ?? "UNKNOWN_ERROR";
}
}
test("M12-01F rejects duplicate identities, cycles, budgets, and unknown fields", async () => {
const duplicateCatalog = structuredClone(v2);
duplicateCatalog.catalogs.push(structuredClone(duplicateCatalog.catalogs[0]));
const duplicateAsset = structuredClone(v2);
duplicateAsset.assets.push(structuredClone(duplicateAsset.assets[0]));
const legacyCycle = structuredClone(v1);
legacyCycle.catalogs[0].parentId = legacyCycle.catalogs[1].id;
const parentMismatch = structuredClone(v2);
parentMismatch.catalogs[0].parentPath = "Characters";
const simpleName = structuredClone(v2);
simpleName.catalogs[0].simpleName = "\u00e9".repeat(32);
const tag = structuredClone(v2);
tag.assets[0].tags = ["\u00e9".repeat(32)];
tag.assets[0].activeTag = 0;
const unknownTop = { ...structuredClone(v2), future: true };
const unknownAsset = structuredClone(v2);
unknownAsset.assets[0].future = true;
const unknownLegacyAsset = structuredClone(v1);
unknownLegacyAsset.assets[0].future = true;
const cases = [
["DUPLICATE_CATALOG_ID", () => v2Protocol.parseAssetCatalogManifestV2(duplicateCatalog)],
["DUPLICATE_ASSET_ID", () => v2Protocol.parseAssetCatalogManifestV2(duplicateAsset)],
["LEGACY_PARENT_CYCLE", () => migration.migrateAssetCatalogV1ToV2(legacyCycle)],
["PARENT_PATH_MISMATCH", () => v2Protocol.parseAssetCatalogManifestV2(parentMismatch)],
["OVERLONG_SIMPLE_NAME_UTF8", () => v2Protocol.parseAssetCatalogManifestV2(simpleName)],
["OVERLONG_TAG_UTF8", () => v2Protocol.parseAssetCatalogManifestV2(tag)],
["V2_UNKNOWN_TOP_LEVEL", () => v2Protocol.parseAssetCatalogManifestV2(unknownTop)],
["V2_UNKNOWN_ASSET_FIELD", () => v2Protocol.parseAssetCatalogManifestV2(unknownAsset)],
["V1_UNKNOWN_ASSET_FIELD", () => migration.migrateAssetCatalogV1ToV2(unknownLegacyAsset)],
];
const actual = [];
for (const [id, run] of cases) {
actual.push({ id, code: await codeFrom(run) });
assert.deepEqual(await v2Protocol.parseAssetCatalogManifestV2(v2), v2, `${id} poisoned the valid parser path`);
}
assert.deepEqual(actual, golden.cases);
assert.equal(golden.nextTask, "M12-01G");
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-01H/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-01H binds page and Worker restart identity evidence", () => {
assert.equal(manifest.task, "M12-01H");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-01I");
assert.deepEqual(manifest.restart.contexts, ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"]);
assert.equal(manifest.restart.catalogOrder.length, manifest.restart.catalogCount);
assert.equal(manifest.restart.assetOrder.length, manifest.restart.assetCount);
assert.match(manifest.restart.manifestSha256, /^[a-f0-9]{64}$/);
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(repoRoot, "web/protocol/asset-catalog-v2.ts");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-v2-unit-"));
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
fs.writeFileSync(path.join(temporary, "asset-catalog-v2.mjs"), result.outputText);
const catalog = await import(pathToFileURL(path.join(temporary, "asset-catalog-v2.mjs")));
const desktop = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01B/canonical.json"), "utf8"));
const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01C/asset-catalog-v2.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01C/manifest.json"), "utf8"));
test("M12-01C binds the desktop and Blender weak-reference sources", () => {
assert.equal(manifest.nextTask, "M12-01D");
for (const artifact of Object.values(manifest.artifacts)) {
const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex");
assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`);
}
const weakReferenceSource = fs.readFileSync(path.join(repoRoot, manifest.artifacts.blenderWeakReferenceSource.path), "utf8");
assert.match(weakReferenceSource, /AssetWeakReference AssetRepresentation::make_weak_reference\(\) const/);
assert.match(weakReferenceSource, /library_relative_identifier\(\) const/);
});
test("M12-01C converts the desktop catalog baseline into schema v2", async () => {
const generated = await catalog.createAssetCatalogManifestV2FromDesktop(desktop, 1);
assert.deepEqual(generated, golden);
assert.deepEqual(generated.catalogs.map((item) => item.path), ["Characters", "Characters/Heroes", "Materials/Metal"]);
assert.deepEqual(generated.assets.map((item) => item.relativeAssetIdentifier), [
"Material/M12 Brushed Metal", "Object/M12 Hero", "World/M12 Uncataloged World",
]);
assert.equal(generated.assets[2].catalogId, null);
assert.equal(generated.assets[0].author, "");
assert.equal(generated.assets[0].sourceSha256, null);
assert.deepEqual(generated.libraries, []);
assert.equal(generated.assets[1].customProperties.find((item) => item.name === "dimensions").type, "FLOAT_ARRAY");
});
test("M12-01C stable IDs bind Blender weak-reference identity", async () => {
const local = await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: null, relativeAssetIdentifier: "Object/M12 Hero" });
const external = await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: "Studio", relativeAssetIdentifier: "hero.blend/Object/M12 Hero" });
assert.match(local, /^asset:[a-f0-9]{64}$/);
assert.match(external, /^asset:[a-f0-9]{64}$/);
assert.notEqual(local, external);
assert.equal(local, golden.assets[1].assetId);
assert.equal(local, await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: null, relativeAssetIdentifier: "Object/M12 Hero" }));
});
test("M12-01C exposes UTF-8 byte and collection budgets", async () => {
assert.equal(catalog.ASSET_CATALOG_SCHEMA, 2);
assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, 63);
assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxTagBytes, 63);
assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxCatalogs, 10_000);
assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxAssets, 100_000);
assert.deepEqual(catalog.ASSET_CATALOG_V2_ID_TYPES, ["ACTION", "COLLECTION", "IMAGE", "MATERIAL", "NODE_GROUP", "OBJECT", "WORLD"]);
assert.deepEqual(await catalog.parseAssetCatalogManifestV2(golden), golden);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-decode-unit-"));
const transpile = (sourceName, outputName, replacements = []) => {
const sourcePath = path.join(root, "web/protocol", sourceName);
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
fs.writeFileSync(path.join(temporary, outputName), replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText));
};
transpile("asset-preview.ts", "asset-preview.mjs");
transpile("asset-preview-decode.ts", "asset-preview-decode.mjs", [['from "./asset-preview"', 'from "./asset-preview.mjs"']]);
const identityProtocol = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs")));
const decode = await import(pathToFileURL(path.join(temporary, "asset-preview-decode.mjs")));
const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8"));
const content = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png"));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02C/manifest.json"), "utf8"));
const buffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
async function identityFor(bytes, changes = {}) {
const base = {
...identity,
content: {
...identity.content,
byteLength: bytes.byteLength,
sha256: sha256(bytes),
...changes,
},
};
delete base.identitySha256;
return identityProtocol.createAssetPreviewIdentity(base);
}
test("M12-02C plans the checked-in PNG before decode", async () => {
assert.equal(manifest.task, "M12-02C");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02D");
assert.equal(manifest.negativeCaseCount, 8);
assert.deepEqual(decode.ASSET_PREVIEW_DECODE_BUDGET, manifest.budget);
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
}
assert.deepEqual(await decode.planAssetPreviewDecode(identity, buffer(content)), {
schemaVersion: 1,
identitySha256: identity.identitySha256,
mimeType: "image/png",
width: 8,
height: 8,
pixelCount: 64,
encodedByteLength: 513,
decodedByteLength: 256,
compressionRatio: 256 / 513,
});
});
test("M12-02C rejects byte, hash, MIME, dimension, pixel, and compression budgets before decode", async () => {
const cases = [];
const changedHash = buffer(content); new Uint8Array(changedHash)[changedHash.byteLength - 1] ^= 1;
cases.push([identity, changedHash, "ASSET_SOURCE_HASH_MISMATCH"]);
cases.push([{ ...identity, content: { ...identity.content, byteLength: 512 } }, buffer(content), "ASSET_PREVIEW_IDENTITY_MISMATCH"]);
cases.push([await identityFor(content, { mimeType: "image/webp" }), buffer(content), "ASSET_MANIFEST_INVALID"]);
const wrongDimensions = Buffer.from(content);
wrongDimensions.writeUInt32BE(9, 16);
cases.push([await identityFor(wrongDimensions), buffer(wrongDimensions), "ASSET_MANIFEST_INVALID"]);
const hugeDimensions = Buffer.from(content);
hugeDimensions.writeUInt32BE(4097, 16);
hugeDimensions.writeUInt32BE(4097, 20);
cases.push([await identityFor(hugeDimensions, { width: 4097, height: 4097 }), buffer(hugeDimensions), "ASSET_BUDGET_EXCEEDED"]);
const ratioDimensions = Buffer.from(content);
ratioDimensions.writeUInt32BE(4096, 16);
ratioDimensions.writeUInt32BE(4096, 20);
cases.push([await identityFor(ratioDimensions, { width: 4096, height: 4096 }), buffer(ratioDimensions), "ASSET_BUDGET_EXCEEDED"]);
const oversized = new Uint8Array(decode.ASSET_PREVIEW_DECODE_BUDGET.maxEncodedBytes + 1);
cases.push([await identityFor(oversized, { width: 1, height: 1 }), oversized.buffer, "ASSET_BUDGET_EXCEEDED"]);
const corrupt = Buffer.from(content); corrupt[0] = 0;
cases.push([await identityFor(corrupt), buffer(corrupt), "ASSET_MANIFEST_INVALID"]);
for (const [manifest, bytes, code] of cases) {
await assert.rejects(decode.planAssetPreviewDecode(manifest, bytes), { code });
assert.equal((await decode.planAssetPreviewDecode(identity, buffer(content))).identitySha256, identity.identitySha256);
}
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02E/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-02E binds payload deduplication and metadata-isolation evidence", () => {
assert.equal(manifest.task, "M12-02E");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02F");
assert.deepEqual(manifest.claims, [
"ONE_CONTENT_HASH_ONE_OPFS_PAYLOAD",
"DISTINCT_ASSET_IDENTITY_PRESERVED",
"DISTINCT_ASSET_METADATA_PRESERVED",
"REOPENED_HEAD_VERIFIED",
]);
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8"));
const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-02H binds desktop, main-thread, Offscreen, and metric evidence", () => {
assert.equal(manifest.task, "M12-02H");
assert.equal(manifest.enablingTask, false);
assert.equal(manifest.parityStateChange, true);
assert.equal(manifest.implementationClass, "LOCAL_EXACT");
assert.equal(manifest.nextTask, "M12-03A");
assert.deepEqual(manifest.backends, ["BLENDER_5_2_DESKTOP", "MAIN_THREAD_CANVAS_2D", "OFFSCREEN_CANVAS_2D"]);
assert.equal(desktop.rgbaSha256, manifest.reference.rgbaSha256);
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
});

View File

@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-identity-unit-"));
const sourcePath = path.join(root, "web/protocol/asset-preview.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "asset-preview.mjs"), transpiled.outputText);
const preview = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs")));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/manifest.json"), "utf8"));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8"));
const sourceBytes = fs.readFileSync(path.join(root, manifest.artifacts.source.path));
const contentBytes = fs.readFileSync(path.join(root, manifest.artifacts.content.path));
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
const stableJSON = (value) => Array.isArray(value)
? `[${value.map(stableJSON).join(",")}]`
: value && typeof value === "object"
? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`
: JSON.stringify(value);
test("M12-02B binds source, content, generator, protocol, and settings artifacts", () => {
assert.equal(manifest.task, "M12-02B");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02C");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
}
assert.equal(sha256(stableJSON(manifest.generatorSettings)), manifest.generatorSettingsSha256);
assert.equal(manifest.generatorSettingsSha256, golden.generator.settingsSha256);
});
test("M12-02B creates and parses the canonical preview identity", async () => {
const { identitySha256, ...base } = golden;
assert.deepEqual(await preview.createAssetPreviewIdentity(base), golden);
assert.deepEqual(await preview.parseAssetPreviewIdentity(golden), golden);
assert.equal(identitySha256, await preview.computeAssetPreviewIdentity(base));
assert.notEqual(golden.source.sha256, golden.content.sha256);
assert.deepEqual(await preview.verifyAssetPreviewIdentity(
golden,
sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength),
contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength),
golden.generator,
), golden);
});
test("M12-02B rejects independent source, content, generator, dimensions, and unknown-field drift", async () => {
const source = sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength);
const content = contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength);
const changedSource = source.slice(0); new Uint8Array(changedSource)[0] ^= 1;
const changedContent = content.slice(0); new Uint8Array(changedContent)[0] ^= 1;
await assert.rejects(preview.verifyAssetPreviewIdentity(golden, changedSource, content, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" });
await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, changedContent, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" });
await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, content, { ...golden.generator, settingsSha256: "f".repeat(64) }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" });
await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, content: { ...golden.content, width: 9 } }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" });
await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, future: true }), { code: "ASSET_MANIFEST_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02D/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-02D binds OPFS-before-catalog ordering and browser evidence", () => {
assert.equal(manifest.task, "M12-02D");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02E");
assert.deepEqual(manifest.commitOrder, ["PRE_DECODE_GATE", "OPFS_WRITE", "OPFS_READBACK", "CATALOG_TRANSACTION"]);
assert.deepEqual(manifest.faultPoints, ["BEFORE_OPFS_WRITE", "AFTER_OPFS_WRITE", "AFTER_CATALOG_PUT"]);
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02F/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-02F binds corrupt-payload quarantine and readable metadata evidence", () => {
assert.equal(manifest.task, "M12-02F");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02G");
assert.deepEqual(manifest.states, ["READY", "QUARANTINED", "REOPENED_QUARANTINED"]);
assert.equal(manifest.failureCode, "ASSET_SOURCE_HASH_MISMATCH");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const root = path.resolve(import.meta.dirname, "../../..");
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02G/manifest.json"), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
test("M12-02G binds final-reference and cross-project reclamation evidence", () => {
assert.equal(manifest.task, "M12-02G");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-02H");
assert.deepEqual(manifest.sequence, ["REMOVE_FIRST_REFERENCE", "RETAIN_PAYLOAD", "REMOVE_FINAL_REFERENCE", "COLLECT_PAYLOAD"]);
assert.equal(manifest.otherProjectPayload, "PRESERVED_READY");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
}
});

View File

@@ -0,0 +1,111 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-operation-identity-unit-"));
const sourcePath = path.join(root, "web/protocol/library-operation-identity.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "library-operation-identity.mjs"), transpiled.outputText);
const identity = await import(pathToFileURL(path.join(temporary, "library-operation-identity.mjs")));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03B/library-operation-bindings.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03B/manifest.json"), "utf8"));
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
const bindingInput = (binding) => {
const { invalidationToken, ...input } = structuredClone(binding);
return input;
};
const stateFor = (binding) => ({
sourceLibraryId: binding.source.sourceLibraryId,
sourceSha256: binding.source.sourceSha256,
sourceGeneration: binding.sourceGeneration,
sourceRevision: binding.sourceRevision,
dependencyClosureSha256: binding.dependencyClosureSha256,
});
test("M12-03B binds the inventory, protocol, golden, and unit artifacts", () => {
assert.equal(manifest.task, "M12-03B");
assert.equal(manifest.enablingTask, true);
assert.equal(manifest.parityStateChange, false);
assert.equal(manifest.nextTask, "M12-03C");
for (const artifact of Object.values(manifest.artifacts)) {
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
}
});
test("M12-03B derives a stable source library ID from locator and bytes", async () => {
const source = await identity.createLibrarySourceIdentity({
sourceLocator: golden.source.sourceLocator,
sourceSha256: golden.source.sourceSha256,
});
assert.deepEqual(source, golden.source);
assert.deepEqual(await identity.parseLibrarySourceIdentity(source), source);
assert.equal(source.sourceLibraryId, await identity.computeLibrarySourceId(source));
assert.notEqual(source.sourceLibraryId, (await identity.createLibrarySourceIdentity({
sourceLocator: `${source.sourceLocator}.moved`,
sourceSha256: source.sourceSha256,
})).sourceLibraryId);
});
test("M12-03B fixes operation-specific owner and read-only semantics", async () => {
const expected = [
["APPEND", "LOCAL_MAIN", false, false],
["LINK", "SOURCE_LIBRARY", true, true],
["LIBRARY_OVERRIDE", "LOCAL_OVERRIDE", false, true],
];
for (let index = 0; index < golden.bindings.length; index++) {
const binding = golden.bindings[index];
assert.deepEqual(await identity.createLibraryOperationBinding(bindingInput(binding)), binding);
assert.deepEqual(await identity.parseLibraryOperationBinding(binding), binding);
assert.deepEqual(await identity.assertLibraryOperationBindingCurrent(binding, stateFor(binding)), binding);
assert.deepEqual([binding.operation, binding.owner.kind, binding.readOnly, binding.referenceReadOnly], expected[index]);
}
assert.equal(new Set(golden.bindings.map((binding) => binding.invalidationToken)).size, 3);
});
test("M12-03B rejects owner, read-only, source, and token substitution", async () => {
const [append, link, override] = golden.bindings;
await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(append), readOnly: true }), { code: "ASSET_MANIFEST_INVALID" });
await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(link), owner: append.owner }), { code: "ASSET_MANIFEST_INVALID" });
await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(override), referenceReadOnly: false }), { code: "ASSET_MANIFEST_INVALID" });
await assert.rejects(identity.createLibraryOperationBinding({
...bindingInput(override),
owner: { ...override.owner, referenceSourceDataBlockId: "Object/Other" },
}), { code: "ASSET_SOURCE_HASH_MISMATCH" });
await assert.rejects(identity.parseLibrarySourceIdentity({ ...golden.source, sourceSha256: "f".repeat(64) }), { code: "ASSET_SOURCE_HASH_MISMATCH" });
await assert.rejects(identity.parseLibraryOperationBinding({ ...append, sourceRevision: 8 }), { code: "REVISION_CONFLICT" });
await assert.rejects(identity.parseLibraryOperationBinding({ ...link, owner: { ...link.owner, sourceDataBlockId: "Object/Other" } }), { code: "ASSET_SOURCE_HASH_MISMATCH" });
await assert.rejects(identity.parseLibraryOperationBinding({ ...override, future: true }), { code: "ASSET_MANIFEST_INVALID" });
});
test("M12-03B invalidates generation, revision, source, and dependency closure drift", async () => {
const binding = golden.bindings[2];
for (const drift of [
{ sourceGeneration: 4 },
{ sourceRevision: 8 },
{ sourceSha256: "c".repeat(64) },
{ dependencyClosureSha256: "d".repeat(64) },
{ sourceLibraryId: `library:${"e".repeat(64)}` },
]) {
await assert.rejects(identity.assertLibraryOperationBindingCurrent(binding, { ...stateFor(binding), ...drift }), { code: "REVISION_CONFLICT" });
}
});
test("M12-03B enforces exact schemas and UTF-8 budgets", async () => {
await assert.rejects(identity.parseLibrarySourceIdentity({ schemaVersion: 2 }), { code: "PROTOCOL_MISMATCH" });
await assert.rejects(identity.createLibrarySourceIdentity({ sourceLocator: "x".repeat(4_097), sourceSha256: "a".repeat(64) }), { code: "ASSET_MANIFEST_INVALID" });
await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(golden.bindings[0]), sourceGeneration: 0 }), { code: "ASSET_MANIFEST_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,81 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import ts from "typescript";
const repoRoot = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(repoRoot, "web/protocol/render-compositor-media-recovery.ts");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const moduleUrl = "data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64");
const recovery = await import(moduleUrl);
const hash = "a".repeat(64);
const codes = {
RENDER: ["OPEN_CANCELLED", "GPU_TEXTURE_BUDGET_EXCEEDED"],
COMPOSITOR: ["COMPOSITOR_CANCELLED", "COMPOSITOR_BUDGET_EXCEEDED"],
MEDIA: ["SEQUENCER_CANCELLED", "SEQUENCER_BUDGET_EXCEEDED"],
};
const report = (domain) => ({
schemaVersion: 1,
domain,
source: { byteLength: 64, sha256: hash },
cancellation: { status: "CANCELLED", code: codes[domain][0], publishedResults: 0, temporaryResourcesAfter: 0 },
restart: {
status: "RECOVERED",
generationBefore: 1,
generationAfter: 2,
identityBefore: hash,
identityAfter: hash,
outputSha256Before: hash,
outputSha256After: hash,
},
budget: { status: "BLOCKED", code: codes[domain][1], retainedIdentityHash: hash, temporaryResourcesAfter: 0 },
release: { status: "RELEASED", releasedBytes: 64, releasedResources: 1, resourcesAfter: 0 },
recovery: { status: "RECOVERED", identityHash: hash, outputSha256: hash, outputBytes: 64, visibleUnits: 4 },
});
test("M11-14 accepts the complete render, compositor and media recovery suite", () => {
const parsed = recovery.parseRenderCompositorMediaRecoverySuite([
report("MEDIA"),
report("RENDER"),
report("COMPOSITOR"),
]);
assert.deepEqual(parsed.map((item) => item.domain), ["RENDER", "COMPOSITOR", "MEDIA"]);
assert.equal(parsed.every((item) => item.release.resourcesAfter === 0), true);
});
test("M11-14 rejects cancellation publication, domain code drift and duplicate domains", () => {
const published = report("RENDER");
published.cancellation.publishedResults = 1;
assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(published), /must be zero/);
const wrongCode = report("MEDIA");
wrongCode.cancellation.code = "COMPOSITOR_CANCELLED";
assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(wrongCode), /cancellation contract/);
assert.throws(() => recovery.parseRenderCompositorMediaRecoverySuite([
report("RENDER"),
report("RENDER"),
report("MEDIA"),
]), /duplicate domains/);
});
test("M11-14 rejects restart drift, budget mutation and incomplete release", () => {
const restartDrift = report("COMPOSITOR");
restartDrift.restart.outputSha256After = "b".repeat(64);
assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(restartDrift), /restart changed/);
const budgetMutation = report("RENDER");
budgetMutation.budget.retainedIdentityHash = "b".repeat(64);
assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(budgetMutation), /budget failure changed/);
const leaked = report("MEDIA");
leaked.release.resourcesAfter = 1;
assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(leaked), /must be zero/);
});