Advance Blender 5.2 web parity through M12-03D
This commit is contained in:
64
web/tests/unit/asset-catalog-compatibility.test.mjs
Normal file
64
web/tests/unit/asset-catalog-compatibility.test.mjs
Normal 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 }));
|
||||
22
web/tests/unit/asset-catalog-indexeddb-migration.test.mjs
Normal file
22
web/tests/unit/asset-catalog-indexeddb-migration.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
60
web/tests/unit/asset-catalog-migration.test.mjs
Normal file
60
web/tests/unit/asset-catalog-migration.test.mjs
Normal 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 }));
|
||||
89
web/tests/unit/asset-catalog-negatives.test.mjs
Normal file
89
web/tests/unit/asset-catalog-negatives.test.mjs
Normal 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 }));
|
||||
23
web/tests/unit/asset-catalog-restart.test.mjs
Normal file
23
web/tests/unit/asset-catalog-restart.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
70
web/tests/unit/asset-catalog-v2.test.mjs
Normal file
70
web/tests/unit/asset-catalog-v2.test.mjs
Normal 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 }));
|
||||
102
web/tests/unit/asset-preview-decode.test.mjs
Normal file
102
web/tests/unit/asset-preview-decode.test.mjs
Normal 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 }));
|
||||
25
web/tests/unit/asset-preview-dedup.test.mjs
Normal file
25
web/tests/unit/asset-preview-dedup.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
21
web/tests/unit/asset-preview-display.test.mjs
Normal file
21
web/tests/unit/asset-preview-display.test.mjs
Normal 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);
|
||||
});
|
||||
70
web/tests/unit/asset-preview-identity.test.mjs
Normal file
70
web/tests/unit/asset-preview-identity.test.mjs
Normal 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 }));
|
||||
21
web/tests/unit/asset-preview-opfs-commit.test.mjs
Normal file
21
web/tests/unit/asset-preview-opfs-commit.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
21
web/tests/unit/asset-preview-quarantine.test.mjs
Normal file
21
web/tests/unit/asset-preview-quarantine.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
21
web/tests/unit/asset-preview-reference-gc.test.mjs
Normal file
21
web/tests/unit/asset-preview-reference-gc.test.mjs
Normal 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);
|
||||
}
|
||||
});
|
||||
111
web/tests/unit/library-operation-identity.test.mjs
Normal file
111
web/tests/unit/library-operation-identity.test.mjs
Normal 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 }));
|
||||
81
web/tests/unit/render-compositor-media-recovery.test.mjs
Normal file
81
web/tests/unit/render-compositor-media-recovery.test.mjs
Normal 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/);
|
||||
});
|
||||
Reference in New Issue
Block a user