Advance M8-M11 parity workflows
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 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
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(), "compositor-unsupported-unit-"));
const sourcePath = path.join(repoRoot, "web/protocol/compositor.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, "compositor.mjs"), transpiled.outputText.replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"'));
const gates = ts.transpileModule(fs.readFileSync(path.join(repoRoot, "web/protocol/capability-gates.ts"), "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: path.join(repoRoot, "web/protocol/capability-gates.ts"),
reportDiagnostics: true,
});
assert.deepEqual(gates.diagnostics, []);
fs.writeFileSync(path.join(temporary, "capability-gates.mjs"), gates.outputText);
const compositor = await import(pathToFileURL(path.join(temporary, "compositor.mjs")));
const node = (id, type, properties = {}, blenderType) => ({ id, type, name: id, properties, ...(blenderType ? { blenderType } : {}) });
const graph = () => ({
schemaVersion: 1,
id: "compositor:unsupported",
name: "Unsupported",
outputNodeId: "out",
resources: [],
nodes: [
node("color", "CONSTANT_COLOR", { color: [0.125, 0.25, 0.5, 0.75] }),
node("out", "COMPOSITE"),
node("glare", "UNSUPPORTED", {}, "CompositorNodeGlare"),
],
links: [{ fromNodeId: "color", fromSocket: "Image", toNodeId: "out", toSocket: "Image" }],
});
test("M11-08 preserves the unsupported graph and blocks CPU execution before evaluation", async () => {
const value = graph();
const before = structuredClone(value);
const gate = compositor.gateCompositorGraph(value, new Set());
assert.equal(gate.status, "BLOCKED");
assert.deepEqual(gate.issues.map((issue) => issue.code), ["COMPOSITOR_NODE_UNSUPPORTED"]);
let cancellationChecks = 0;
assert.throws(
() => compositor.executeCompositorGraph(value, new Map(), { width: 2, height: 2, cancelled: () => { cancellationChecks++; return false; } }),
{ code: "COMPOSITOR_NODE_UNSUPPORTED" },
);
assert.equal(cancellationChecks, 0);
assert.deepEqual(value, before);
});
test("M11-08 blocks a cached unsupported graph before a cache hit", async () => {
const value = graph();
const before = structuredClone(value);
const cache = new compositor.CompositorFrameCache(512);
const key = await compositor.compositorFrameCacheKey(value, new Map(), 1, 2, 2);
cache.set(key, {
composite: { width: 2, height: 2, data: new Float32Array(16), colorSpace: "LINEAR_SRGB" },
viewers: new Map(),
evaluatedNodeIds: ["cached"],
});
await assert.rejects(
() => compositor.executeCompositorGraphCached(value, new Map(), cache, { frame: 1, width: 2, height: 2 }),
{ code: "COMPOSITOR_NODE_UNSUPPORTED" },
);
assert.equal(cache.size, 1);
assert.deepEqual(value, before);
});
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 root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "compositor-webgpu-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(root, "web/protocol", sourceName);
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 output = replacements.reduce((value, [from, to]) => value.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("compositor.ts", "compositor.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
]);
const compositor = await import(pathToFileURL(path.join(temporary, "compositor.mjs")));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-07/compositor-node-golden.json"), "utf8"));
const sourceColor = [0.125, 0.25, 0.5, 0.75];
function graph(nodeTypes, extraNodes = [], resources = []) {
const nodes = nodeTypes.map((type, index) => ({
id: `node:${index}`,
type,
name: `${type}:${index}`,
properties: type === "CONSTANT_COLOR" ? { color: sourceColor } : type === "EXPOSURE" ? { exposure: 1 } : {},
}));
return {
schemaVersion: 1,
id: `graph:${nodeTypes.join("-")}`,
name: "M11-07 unit",
outputNodeId: nodes.at(-1).id,
nodes: [...nodes, ...extraNodes],
links: nodes.slice(1).map((node, index) => ({ fromNodeId: nodes[index].id, fromSocket: "Image", toNodeId: node.id, toSocket: "Image" })),
resources,
};
}
function repeatedPixel(pixel, count) {
const result = new Float32Array(count * 4);
for (let index = 0; index < count; index++) result.set(pixel, index * 4);
return result;
}
test("M11-07 freezes only nodes with an independent CPU/WebGPU golden", () => {
assert.deepEqual(compositor.COMPOSITOR_WEBGPU_NODE_ALLOWLIST, golden.allowlist);
for (const candidate of golden.cases) {
const value = graph(candidate.nodeTypes);
const plan = compositor.compileCompositorWebGPUPlan(value);
assert.deepEqual(plan.instructions.map((instruction) => instruction.type), candidate.nodeTypes);
const cpu = compositor.executeCompositorGraph(value, new Map(), { width: golden.width, height: golden.height });
const expected = repeatedPixel(candidate.pixel, golden.width * golden.height);
assert.deepEqual(cpu.composite.data, expected, candidate.scene);
assert.equal(crypto.createHash("sha256").update(new Uint8Array(cpu.composite.data.buffer)).digest("hex"), candidate.float32Sha256);
}
});
test("M11-07 rejects every CPU-only or undeclared node before WebGPU compilation", () => {
const properties = { ALPHA_OVER: {}, BLUR: { radius: 1 }, IMAGE: { resourceId: "resource:test" }, MIX: { factor: 0.5 }, RENDER_LAYER: { resourceId: "resource:test" }, TRANSFORM: {}, UNSUPPORTED: {}, VIEWER: {} };
for (const type of golden.blockedNodeTypes) {
const node = { id: `blocked:${type}`, type, name: type, properties: properties[type], ...(type === "UNSUPPORTED" ? { blenderType: "CompositorNodeGlare" } : {}) };
const resources = type === "IMAGE" || type === "RENDER_LAYER" ? [{ id: "resource:test", kind: type, sourceId: "image:test" }] : [];
assert.throws(() => compositor.compileCompositorWebGPUPlan(graph(["CONSTANT_COLOR", "COMPOSITE"], [node], resources)), { code: "COMPOSITOR_NODE_UNSUPPORTED" });
}
});
test("M11-07 rejects disconnected allowlisted nodes and non-Image links", () => {
assert.throws(
() => compositor.compileCompositorWebGPUPlan(graph(["CONSTANT_COLOR", "COMPOSITE"], [{ id: "extra", type: "INVERT", name: "extra", properties: {} }])),
{ code: "COMPOSITOR_GRAPH_INVALID" },
);
const malformed = graph(["CONSTANT_COLOR", "EXPOSURE", "COMPOSITE"]);
malformed.links[0].toSocket = "Value";
assert.throws(() => compositor.compileCompositorWebGPUPlan(malformed), { code: "COMPOSITOR_GRAPH_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,142 @@
import assert from "node:assert/strict";
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/curve-topology-editor.ts");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "curve-topology-editor-unit-"));
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 modulePath = path.join(temporary, "curve-topology-editor.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const curve = await import(pathToFileURL(modulePath));
function claim(operator, selection) {
const base = {
schemaVersion: 1,
operator,
dataId: "curve:Curve",
baseRevision: 17,
inputSplineCount: 4,
inputPointCount: 12,
selectedSplineIndices: selection === "SPLINES" || selection === "POINTS_OR_SPLINES" ? [1] : [],
selectedPointIndices: selection === "POINTS" ? [2] : [],
addedSplineCount: 0,
addedPointCount: 0,
outputSplineCount: 4,
outputPointCount: 12,
payloadBytes: 256,
};
if (operator === "ADD_SPLINE") return { ...base, addedSplineCount: 1, addedPointCount: 2, outputSplineCount: 5, outputPointCount: 14 };
if (operator === "DUPLICATE") return { ...base, addedSplineCount: 1, addedPointCount: 2, outputSplineCount: 5, outputPointCount: 14 };
if (operator === "EXTRUDE") return { ...base, addedPointCount: 1, outputPointCount: 13 };
if (operator === "SPLIT") return { ...base, addedSplineCount: 1, outputSplineCount: 5 };
if (operator === "SUBDIVIDE") return { ...base, addedPointCount: 2, outputPointCount: 14, subdivideCuts: 2 };
return base;
}
test("M9-04 freezes the Blender-sourced Curve topology allowlist and budgets", () => {
const manifest = curve.createCurveTopologyEditorManifest();
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.sourceAuthority, "blender-5.2.0/source/blender/editors/curve/curve_ops.cc");
assert.equal(manifest.atomicMainTransaction, true);
assert.deepEqual(manifest.budget, {
maxSplines: 65_536,
maxPoints: 1_000_000,
maxSelectedElements: 100_000,
maxAddedSplinesPerOperation: 4_096,
maxAddedPointsPerOperation: 100_000,
maxPayloadBytes: 67_108_864,
maxSubdivideCuts: 64,
maxDataIdBytes: 256,
maxOperationsPerMainTransaction: 1,
});
assert.deepEqual(manifest.operators.map((operator) => operator.id), [
"ADD_SPLINE", "DECIMATE", "DELETE", "DISSOLVE_VERTICES", "DUPLICATE", "EXTRUDE",
"MAKE_SEGMENT", "SEPARATE", "SET_HANDLE_TYPE", "SET_SPLINE_TYPE", "SPLIT", "SUBDIVIDE",
"SWITCH_DIRECTION", "TOGGLE_CYCLIC",
]);
assert.deepEqual(manifest.operators.filter((operator) => operator.gate.status === "READY").map((operator) => operator.id), ["TOGGLE_CYCLIC"]);
assert.ok(manifest.operators.filter((operator) => operator.id !== "TOGGLE_CYCLIC").every((operator) => operator.gate.status === "BLOCKED" && operator.gate.reasonCode === "CURVE_TOPOLOGY_OPERATOR_NOT_VERIFIED"));
});
test("M9-04 accepts one revision-bound budget claim for every allowlisted operator", () => {
for (const descriptor of curve.CURVE_TOPOLOGY_EDITOR_OPERATORS) {
const parsed = curve.parseCurveTopologyOperationClaim(claim(descriptor.id, descriptor.selection), 17);
assert.equal(parsed.operator, descriptor.id);
assert.equal(parsed.baseRevision, 17);
}
});
test("M9-04 rejects unknown, stale, ambiguous and over-budget Curve topology claims", () => {
const valid = claim("SUBDIVIDE", "POINTS_OR_SPLINES");
const cases = [
[{ ...valid, operator: "SPIN" }, "NON_MESH_TOPOLOGY_EDIT_UNSUPPORTED"],
[valid, "REVISION_CONFLICT", 18],
[{ ...valid, selectedSplineIndices: [1, 1] }, "NON_MESH_PROPERTY_INVALID"],
[{ ...valid, selectedPointIndices: [2] }, "NON_MESH_PROPERTY_INVALID"],
[{ ...valid, dataId: "data:Curve" }, "NON_MESH_PROPERTY_INVALID"],
[{ ...claim("DELETE", "POINTS_OR_SPLINES"), inputSplineCount: 0, selectedSplineIndices: [0] }, "NON_MESH_PROPERTY_INVALID"],
[{ ...valid, outputPointCount: 15 }, "NON_MESH_PROPERTY_INVALID"],
[{ ...valid, payloadBytes: curve.CURVE_TOPOLOGY_EDITOR_BUDGET.maxPayloadBytes + 1 }, "NON_MESH_DATA_BUDGET_EXCEEDED"],
[{ ...valid, subdivideCuts: 65 }, "NON_MESH_DATA_BUDGET_EXCEEDED"],
[{ ...claim("DELETE", "POINTS_OR_SPLINES"), subdivideCuts: 1 }, "NON_MESH_PROPERTY_INVALID"],
[{ ...valid, futureField: true }, "NON_MESH_PROPERTY_INVALID"],
];
for (const [value, code, revision = 17] of cases) {
assert.throws(() => curve.parseCurveTopologyOperationClaim(value, revision), (error) => error.code === code);
}
});
test("M9-05 builds one revision-bound TOGGLE_CYCLIC Main command", () => {
const operation = curve.buildCurveToggleCyclicOperation({
schemaVersion: 1,
dataId: "curve:WebCurveData",
baseRevision: 23,
splineIndex: 1,
splineCount: 2,
pointCount: 7,
cyclicU: [false, true],
}, 23);
assert.deepEqual(operation.command, {
type: "setCurveTopology",
dataId: "curve:WebCurveData",
baseRevision: 23,
cyclicU: [false, false],
});
assert.equal(operation.claim.operator, "TOGGLE_CYCLIC");
assert.deepEqual(operation.claim.selectedSplineIndices, [1]);
assert.equal(operation.previousCyclic, true);
assert.equal(operation.nextCyclic, false);
});
test("M9-05 keeps stale and malformed TOGGLE_CYCLIC requests out of Main", () => {
const valid = {
schemaVersion: 1,
dataId: "curve:WebCurveData",
baseRevision: 23,
splineIndex: 0,
splineCount: 2,
pointCount: 7,
cyclicU: [false, false],
};
assert.throws(() => curve.buildCurveToggleCyclicOperation(valid, 24), (error) => error.code === "REVISION_CONFLICT");
for (const value of [
{ ...valid, splineIndex: 2 },
{ ...valid, splineCount: 0 },
{ ...valid, cyclicU: [false] },
{ ...valid, dataId: "surface:WebSurfaceData" },
]) {
assert.throws(() => curve.buildCurveToggleCyclicOperation(value, 23), (error) => error.code === "NON_MESH_PROPERTY_INVALID");
}
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,66 @@
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/diagnostic-report.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 { APP_DIAGNOSTIC_MESSAGES, appendAppDiagnostic, createAppDiagnosticEntry, createAppDiagnosticReport } = await import(moduleUrl);
test("M7-17 keeps user summaries stable while retaining detailed causes", () => {
const cause = new Error("native allocator detail");
const error = new Error("WORKER_CRASH_INJECTED: engine.worker.ts:91", { cause });
error.code = "WORKER_TERMINATED";
const entry = createAppDiagnosticEntry({
sequence: 1,
occurredAt: "2026-08-15T20:00:00.000Z",
area: "ENGINE",
code: "ENGINE_WORKER_TERMINATED",
error,
context: { projectId: "basic_scene", revision: 9 },
});
assert.equal(entry.summary, APP_DIAGNOSTIC_MESSAGES.ENGINE_WORKER_TERMINATED);
assert.equal(entry.summary.includes("WORKER_CRASH_INJECTED"), false);
assert.match(entry.detail, /WORKER_CRASH_INJECTED/);
assert.equal(entry.sourceCode, "WORKER_TERMINATED");
assert.equal(entry.cause, "native allocator detail");
});
test("M7-17 bounds the ledger and exports a sequence-sorted schema v1 report", () => {
const entries = [3, 1, 2].map((sequence) => createAppDiagnosticEntry({
sequence,
occurredAt: `2026-08-15T20:00:0${sequence}.000Z`,
area: "STORAGE",
code: "STORAGE_BUDGET_FAILED",
error: { code: "STORAGE_TRANSACTION", message: `detail-${sequence}` },
}));
assert.deepEqual(appendAppDiagnostic(entries.slice(0, 2), entries[2], 2).map((entry) => entry.sequence), [1, 2]);
const report = createAppDiagnosticReport({
generatedAt: "2026-08-15T20:01:00.000Z",
runtime: { url: "http://127.0.0.1:5173/", userAgent: "test", language: "zh-CN", crossOriginIsolated: true },
project: { projectId: "basic_scene", revision: 9 },
entries,
});
assert.equal(report.schemaVersion, 1);
assert.equal(report.product, "Web Blender Modeler V1");
assert.deepEqual(report.entries.map((entry) => entry.sequence), [1, 2, 3]);
assert.deepEqual(report.entries.map((entry) => entry.detail), ["detail-1", "detail-2", "detail-3"]);
});
test("M7-17 user-visible status setters never interpolate raw exception detail", () => {
const appSource = fs.readFileSync(path.join(repoRoot, "web/app/src/app/App.tsx"), "utf8");
const statusLines = appSource.split("\n").filter((line) => /set(?:Engine|Storage|Wasm|Manifest)Status\(|setViewportError\(/.test(line));
assert.ok(statusLines.length > 20, "expected to audit all user-visible status boundaries");
for (const line of statusLines) {
assert.doesNotMatch(line, /error\.message|errorMessage\(|fault\.error\.message/, line.trim());
}
assert.doesNotMatch(appSource, /\{fault\.error\.message\}/);
});

View File

@@ -0,0 +1,62 @@
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/editing-domain-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 base = (domain, dataId) => ({
schemaVersion: 1,
domain,
baseline: { objectIds: [`object:${domain}`], dataIds: [dataId], objectCount: 1, revision: 4, identityHash: hash },
workerRestart: { status: "RECOVERED", workerGeneration: 2, revisionBefore: 4, revisionAfter: 4, hashBefore: hash, hashAfter: hash, liveHandles: 1, temporaryResourcesAfter: 0 },
oom: { status: "RECOVERED", faultPoint: "GPU_GEOMETRY_UPLOAD", code: "GPU_GEOMETRY_BUDGET_EXCEEDED", revisionBefore: 4, revisionAfter: 4, hashBefore: hash, hashAfter: hash, releasedBytes: 4096, temporaryResourcesAfter: 0 },
gpuRelease: { status: "RECOVERED", backend: "WEBGL2", releaseCount: 1, reinitCount: 1, disposedResources: 6, visiblePixels: 12, pixelHashBefore: hash, pixelHashAfter: hash },
smallScene: { status: "RECOVERED", revision: 4, identityHash: hash, objectCount: 1, dataIds: [dataId], visiblePixels: 12 },
});
test("M9-14 parses all three editing domains and preserves recovery invariants", () => {
const reports = recovery.parseEditingDomainRecoverySuite([
base("CURVE", "curve:Recovery"),
base("GREASE_PENCIL", "grease-pencil:Recovery"),
base("PAINT", "mesh:Recovery"),
]);
assert.deepEqual(reports.map((report) => report.domain), ["CURVE", "GREASE_PENCIL", "PAINT"]);
assert.equal(reports.every((report) => report.workerRestart.hashAfter === report.baseline.identityHash), true);
assert.equal(reports.every((report) => report.oom.releasedBytes > 0 && report.gpuRelease.visiblePixels > 0), true);
});
test("M9-14 rejects stale identity, duplicate domains and GPU release drift", () => {
const curve = base("CURVE", "curve:Recovery");
const stale = structuredClone(curve);
stale.smallScene.identityHash = "b".repeat(64);
assert.throws(() => recovery.parseEditingDomainRecoveryEvidence(stale), /smallScene identity/);
assert.throws(() => recovery.parseEditingDomainRecoverySuite([curve, curve, base("PAINT", "mesh:Recovery")]), /duplicate editing domain/);
const releaseDrift = structuredClone(curve);
releaseDrift.gpuRelease.releaseCount = 2;
assert.throws(() => recovery.parseEditingDomainRecoveryEvidence(releaseDrift), /release and reinitialize exactly once/);
});
test("M9-14 summarizes only visible objects in the requested editing domain", () => {
const snapshot = {
nodes: [
{ id: "object:curve", type: "CURVE", visible: true, dataId: "curve:one" },
{ id: "object:hidden", type: "CURVE", visible: false, dataId: "curve:two" },
{ id: "object:mesh", type: "MESH", visible: true, dataId: "mesh:one" },
],
};
assert.deepEqual(recovery.summarizeEditingDomain(snapshot, "CURVE"), { objectIds: ["object:curve"], dataIds: ["curve:one"], objectCount: 1 });
assert.deepEqual(recovery.summarizeEditingDomain(snapshot, "PAINT"), { objectIds: ["object:mesh"], dataIds: ["mesh:one"], objectCount: 1 });
assert.throws(() => recovery.summarizeEditingDomain({ nodes: [] }, "GREASE_PENCIL"), /DOMAIN_MISSING/);
});

View File

@@ -0,0 +1,165 @@
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(), "external-vfont-unit-"));
for (const name of ["asset-path", "external-vfont"]) {
const sourcePath = path.join(repoRoot, `web/protocol/${name}.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, `${name}.mjs`), transpiled.outputText.replace('"./asset-path"', '"./asset-path.mjs"'));
}
const font = await import(pathToFileURL(path.join(temporary, "external-vfont.mjs")));
const importSourcePath = path.join(repoRoot, "web/app/src/fonts/external-vfont-import.ts");
const importTranspiled = ts.transpileModule(fs.readFileSync(importSourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: importSourcePath,
reportDiagnostics: true,
});
assert.deepEqual(importTranspiled.diagnostics, []);
fs.writeFileSync(path.join(temporary, "external-vfont-import.mjs"), importTranspiled.outputText.replace('"../../../protocol/external-vfont"', '"./external-vfont.mjs"'));
const importer = await import(pathToFileURL(path.join(temporary, "external-vfont-import.mjs")));
const pfb = fs.readFileSync(path.join(repoRoot, "blender-5.2.0/release/datafiles/bfont.pfb"));
const data = pfb.buffer.slice(pfb.byteOffset, pfb.byteOffset + pfb.byteLength);
const digest = crypto.createHash("sha256").update(pfb).digest("hex");
const request = { sourcePath: "//fonts/bfont.pfb", mimeType: "application/x-font-type1", byteLength: pfb.byteLength, sha256: digest, data };
test("M9-01 validates a real project-relative PFB before storage or Main mutation", async () => {
const protocolSource = fs.readFileSync(path.join(repoRoot, "web/protocol/external-vfont.ts"), "utf8");
assert.doesNotMatch(protocolSource, /StorageClient|WebEngineClient|storage\.worker|web-engine\.worker/);
const validated = await font.validateExternalVFontImport(request);
assert.deepEqual({ ...validated, data: undefined }, {
schemaVersion: 1,
sourcePath: "//fonts/bfont.pfb",
fileName: "bfont.pfb",
format: "PFB",
mimeType: "application/x-font-type1",
byteLength: 25181,
sha256: "a33954fdab9fb09b9d308cb7f970518293128922ffc523c0a22b3b314a9a56c6",
data: undefined,
});
assert.notEqual(validated.data, request.data);
assert.deepEqual(new Uint8Array(validated.data), new Uint8Array(request.data));
});
test("M9-01 blocks path escape, type spoofing, size overflow and hash drift", async () => {
const cases = [
[{ ...request, sourcePath: "../../outside.pfb" }, "NON_MESH_RESOURCE_OUTSIDE_PROJECT"],
[{ ...request, sourcePath: "//assets/bfont.pfb" }, "NON_MESH_RESOURCE_OUTSIDE_PROJECT"],
[{ ...request, sourcePath: "//fonts/bfont.ttf", mimeType: "font/ttf" }, "NON_MESH_BINARY_INVALID"],
[{ ...request, mimeType: "font/otf" }, "NON_MESH_BINARY_INVALID"],
[{ ...request, byteLength: font.EXTERNAL_VFONT_MAX_BYTES + 1 }, "NON_MESH_DATA_BUDGET_EXCEEDED"],
[{ ...request, byteLength: request.byteLength - 1 }, "NON_MESH_BINARY_INVALID"],
[{ ...request, sha256: "0".repeat(64) }, "ASSET_SOURCE_HASH_MISMATCH"],
];
for (const [value, code] of cases) await assert.rejects(font.validateExternalVFontImport(value), (error) => error.code === code);
});
test("M9-02 accepts only a matching OPFS content-addressed receipt before Main import", async () => {
const validated = await font.validateExternalVFontImport(request);
const stored = {
assetId: `sha256:${digest}`,
projectId: "m9-vfont-unit",
sha256: digest,
bytes: pfb.byteLength,
mimeType: request.mimeType,
sourcePath: "fonts/bfont.pfb",
path: `projects/m9-vfont-unit/assets/sha256/${digest.slice(0, 2)}/${digest}`,
createdAt: "2026-08-16T00:00:00.000Z",
lastAccessAt: "2026-08-16T00:00:00.000Z",
persisted: true,
deduplicated: false,
};
const mainImport = font.createExternalVFontMainImport(validated, stored);
assert.deepEqual({ ...mainImport, data: undefined }, {
schemaVersion: 1,
projectId: stored.projectId,
assetId: stored.assetId,
assetPath: stored.path,
sourcePath: "//fonts/bfont.pfb",
name: "bfont",
format: "PFB",
mimeType: request.mimeType,
byteLength: pfb.byteLength,
sha256: digest,
data: undefined,
});
assert.notEqual(mainImport.data, validated.data);
for (const receipt of [
{ ...stored, persisted: false },
{ ...stored, sha256: "0".repeat(64) },
{ ...stored, bytes: stored.bytes - 1 },
{ ...stored, path: `indexeddb:${stored.projectId}:${digest}` },
]) {
assert.throws(() => font.createExternalVFontMainImport(validated, receipt), (error) =>
error.code === "ASSET_SOURCE_HASH_MISMATCH" || error.code === "NON_MESH_RESOURCE_MISSING");
}
});
test("M9-03 verifies the project asset before one Main font-style replacement", async () => {
const stored = {
asset: {
assetId: `sha256:${digest}`,
projectId: "m9-vfont-unit",
sha256: digest,
bytes: pfb.byteLength,
mimeType: request.mimeType,
sourcePath: "fonts/bfont.pfb",
path: `projects/m9-vfont-unit/assets/sha256/${digest.slice(0, 2)}/${digest}`,
createdAt: "2026-08-16T00:00:00.000Z",
lastAccessAt: "2026-08-16T00:00:00.000Z",
},
data: data.slice(0),
};
const vfont = { id: "vfont:bfont", name: "bfont", sourcePath: "//fonts/bfont.pfb", builtin: false, packed: true, packedByteLength: pfb.byteLength, sha256: digest };
const original = { regular: "vfont:Bfont", bold: "vfont:Bfont", italic: "vfont:Bfont", boldItalic: "vfont:Bfont" };
const snapshot = { nonMeshData: [{ id: "data:font", type: "FONT", fontLinks: original }], vfonts: [vfont] };
const events = [];
const result = await importer.replaceExternalVFontStyleInMain({
projectId: stored.asset.projectId,
sha256: digest,
dataId: "data:font",
vfontId: vfont.id,
style: "regular",
snapshot,
storage: { readAsset: async () => { events.push("storage:verified"); return stored; } },
engine: { applyCommand: async (command) => {
events.push("main:committed");
return { snapshot: { ...snapshot, nonMeshData: [{ ...snapshot.nonMeshData[0], fontLinks: command.links }] } };
} },
});
assert.deepEqual(events, ["storage:verified", "main:committed"]);
assert.deepEqual(result.previousLinks, original);
assert.equal(result.links.regular, vfont.id);
});
test("M9-03 blocks a missing project asset before Main replacement", async () => {
let mainCalls = 0;
const snapshot = {
nonMeshData: [{ id: "data:font", type: "FONT", fontLinks: { regular: "vfont:Bfont", bold: "vfont:Bfont", italic: "vfont:Bfont", boldItalic: "vfont:Bfont" } }],
vfonts: [{ id: "vfont:bfont", name: "bfont", sourcePath: "//fonts/bfont.pfb", builtin: false, packed: true, packedByteLength: pfb.byteLength, sha256: digest }],
};
await assert.rejects(importer.replaceExternalVFontStyleInMain({
projectId: "m9-vfont-unit",
sha256: digest,
dataId: "data:font",
vfontId: "vfont:bfont",
style: "regular",
snapshot,
storage: { readAsset: async () => { const error = new Error("missing"); error.code = "NON_MESH_RESOURCE_MISSING"; throw error; } },
engine: { applyCommand: async () => { mainCalls += 1; throw new Error("unexpected Main call"); } },
}), (error) => error.code === "NON_MESH_RESOURCE_MISSING");
assert.equal(mainCalls, 0);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,258 @@
import assert from "node:assert/strict";
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(), "geometry-nodes-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("geometry-nodes.ts", "geometry-nodes.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
]);
const geometryNodes = await import(pathToFileURL(path.join(temporary, "geometry-nodes.mjs")));
function graph() {
return {
schemaVersion: 1,
id: "node-group:Unit",
name: "Unit",
interfaceInputs: [{ id: "input:geometry", name: "Geometry", direction: "INPUT", dataType: "GEOMETRY" }],
interfaceOutputs: [{ id: "output:geometry", name: "Geometry", direction: "OUTPUT", dataType: "GEOMETRY" }],
nodes: [{
id: "geometry-node:7",
type: "GeometryNodeTransform",
name: "Transform Geometry",
sockets: [
{ id: "input:mode", name: "Mode", direction: "INPUT", dataType: "MENU", defaultValue: 0 },
{ id: "input:rotation", name: "Rotation", direction: "INPUT", dataType: "ROTATION", defaultValue: [0, 0, 0] },
{ id: "input:matrix", name: "Transform", direction: "INPUT", dataType: "MATRIX" },
{ id: "output:geometry", name: "Geometry", direction: "OUTPUT", dataType: "GEOMETRY" },
],
}],
links: [],
groupReferences: [],
graphHash: "a".repeat(64),
};
}
test("M10-01 parses Blender Main socket types and stable graph identities", () => {
const parsed = geometryNodes.parseGeometryNodeGraph(graph());
assert.equal(parsed.id, "node-group:Unit");
assert.deepEqual(parsed.nodes[0].sockets.map((socket) => socket.dataType), ["MENU", "ROTATION", "MATRIX", "GEOMETRY"]);
assert.equal(geometryNodes.validateGeometryNodeGraph(parsed).status, "SUPPORTED");
assert.deepEqual(geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET, {
maxGraphs: 4_096,
maxNodesPerGraph: 4_096,
maxLinksPerGraph: 16_384,
maxSocketsPerGraph: 65_536,
maxInterfaceSocketsPerGraph: 4_096,
maxIdentifierBytes: 256,
maxNameBytes: 1_024,
});
});
test("M10-01 rejects duplicate stable IDs and malformed graph hashes", () => {
const duplicateNode = graph();
duplicateNode.nodes.push(structuredClone(duplicateNode.nodes[0]));
assert.throws(() => geometryNodes.parseGeometryNodeGraph(duplicateNode), { code: "GN_INVALID_GRAPH" });
const duplicateSocket = graph();
duplicateSocket.nodes[0].sockets.push(structuredClone(duplicateSocket.nodes[0].sockets[0]));
assert.throws(() => geometryNodes.parseGeometryNodeGraph(duplicateSocket), { code: "GN_INVALID_GRAPH" });
assert.throws(() => geometryNodes.parseGeometryNodeGraph({ ...graph(), graphHash: "A".repeat(64) }), { code: "GN_INVALID_GRAPH" });
});
test("M10-01 fails closed before oversized graph topology enters SceneIR", () => {
const oversized = graph();
oversized.nodes = Array.from({ length: geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET.maxNodesPerGraph + 1 },
(_value, index) => ({ id: `node:${index}`, type: "NodeGroupInput", name: "Input", sockets: [] }));
assert.throws(() => geometryNodes.parseGeometryNodeGraph(oversized), { code: "GN_GRAPH_BUDGET_EXCEEDED" });
const graphSet = Array.from({ length: geometryNodes.GEOMETRY_NODE_GRAPH_BUDGET.maxGraphs + 1 }, graph);
assert.deepEqual(geometryNodes.validateGeometryNodeGraphSet(graphSet).issues.map((issue) => issue.code), ["GN_GRAPH_BUDGET_EXCEEDED"]);
});
test("M10-02 freezes the allowlist and blocks unsupported nodes without rewriting the graph", () => {
const allowed = graph();
const before = structuredClone(allowed);
assert.equal(geometryNodes.GEOMETRY_NODE_ALLOWLIST_SCHEMA, 1);
assert.deepEqual(geometryNodes.GEOMETRY_NODE_ALLOWLIST, [
"NodeGroupInput",
"NodeGroupOutput",
"GeometryNodeTransform",
"GeometryNodeSetPosition",
"GeometryNodeJoinGeometry",
"GeometryNodeSeparateGeometry",
"GeometryNodeRealizeInstances",
"GeometryNodeStoreNamedAttribute",
"FunctionNodeInputInt",
"FunctionNodeInputVector",
"FunctionNodeCompare",
"ShaderNodeValue",
"ShaderNodeMath",
"GeometryNodeObjectInfo",
"GeometryNodeCollectionInfo",
"GeometryNodeImageInfo",
]);
assert.equal(geometryNodes.gateGeometryNodeGraph(allowed).status, "READY");
allowed.nodes.push({
id: "geometry-node:8",
type: "GeometryNodeSimulationOutput",
name: "Simulation Output",
sockets: [],
});
const blocked = geometryNodes.gateGeometryNodeGraph(allowed);
assert.equal(blocked.status, "BLOCKED");
assert.deepEqual(blocked.issues.map((issue) => issue.code), ["GN_NODE_UNSUPPORTED"]);
assert.deepEqual(before, graph());
assert.equal(allowed.nodes.at(-1).type, "GeometryNodeSimulationOutput");
});
function domainCardinality(overrides = {}) {
return {
POINT: 8,
EDGE: 12,
FACE: 6,
CORNER: 24,
CURVE: 0,
INSTANCE: 0,
LAYER: 0,
...overrides,
};
}
function field(overrides = {}) {
return {
schemaVersion: 1,
graphId: "node-group:Unit",
graphHash: "a".repeat(64),
fieldId: "field:position",
revision: 7,
sourceDomain: "POINT",
targetDomain: "CORNER",
dataType: "FLOAT",
transport: "JSON",
domainCardinality: domainCardinality(),
...overrides,
};
}
test("M10-04 binds field materialization to exact domain cardinality and byte budgets", () => {
assert.equal(geometryNodes.GEOMETRY_NODE_FIELD_SCHEMA, 1);
assert.deepEqual(geometryNodes.GEOMETRY_NODE_FIELD_DOMAIN_BUDGET, {
POINT: 1_000_000,
EDGE: 2_000_000,
FACE: 2_000_000,
CORNER: 4_000_000,
CURVE: 100_000,
INSTANCE: 100_000,
LAYER: 4_096,
});
assert.deepEqual(geometryNodes.GEOMETRY_NODE_FIELD_BUDGET, {
maxFieldsPerBatch: 64,
maxDomainConversionsPerBatch: 32,
maxMaterializedElementsPerBatch: 4_000_000,
maxMaterializedBytesPerBatch: 64 * 1024 * 1024,
maxJsonScalarValuesPerField: 65_536,
maxIdentifierBytes: 256,
});
const batch = geometryNodes.parseGeometryNodeFieldMaterializationBatch([
field(),
field({
fieldId: "field:offset",
sourceDomain: "CONSTANT",
targetDomain: "POINT",
dataType: "VECTOR",
}),
]);
assert.equal(batch.fieldCount, 2);
assert.equal(batch.domainConversionCount, 1);
assert.equal(batch.materializedElementCount, 32);
assert.equal(batch.materializedByteLength, 192);
assert.deepEqual(batch.fields.map((entry) => ({
source: entry.sourceElementCount,
target: entry.targetElementCount,
scalars: entry.scalarValueCount,
bytes: entry.materializedByteLength,
conversion: entry.domainConversion,
})), [
{ source: 8, target: 24, scalars: 24, bytes: 96, conversion: true },
{ source: 1, target: 8, scalars: 24, bytes: 96, conversion: false },
]);
});
test("M10-04 blocks unbounded JSON fields, cardinality drift and aggregate overflow", () => {
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch({}), { code: "GN_INVALID_GRAPH" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization({
...field(),
values: Array(24).fill(0),
}), { code: "GN_FIELD_JSON_BUDGET_EXCEEDED" });
const largeCardinality = domainCardinality({ POINT: 100_000, CORNER: 300_000 });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({
targetDomain: "POINT",
dataType: "VECTOR",
domainCardinality: largeCardinality,
})), { code: "GN_FIELD_JSON_BUDGET_EXCEEDED" });
assert.equal(geometryNodes.parseGeometryNodeFieldMaterialization(field({
targetDomain: "POINT",
dataType: "VECTOR",
transport: "BINARY",
domainCardinality: largeCardinality,
})).materializedByteLength, 1_200_000);
const missingDomain = domainCardinality();
delete missingDomain.LAYER;
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({
domainCardinality: missingDomain,
})), { code: "GN_DOMAIN_CARDINALITY_MISMATCH" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({
domainCardinality: { ...domainCardinality(), VOXEL: 1 },
})), { code: "GN_DOMAIN_CARDINALITY_MISMATCH" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({
dataType: "toString",
})), { code: "GN_INVALID_GRAPH" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterialization(field({
domainCardinality: domainCardinality({ EDGE: 2_000_001 }),
})), { code: "GN_FIELD_BUDGET_EXCEEDED" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch([
field({
fieldId: "field:large-a",
targetDomain: "CORNER",
dataType: "COLOR",
transport: "BINARY",
domainCardinality: domainCardinality({ CORNER: 2_500_000 }),
}),
field({
fieldId: "field:large-b",
targetDomain: "CORNER",
dataType: "COLOR",
transport: "BINARY",
domainCardinality: domainCardinality({ CORNER: 2_500_000 }),
}),
]), { code: "GN_FIELD_BUDGET_EXCEEDED" });
assert.throws(() => geometryNodes.parseGeometryNodeFieldMaterializationBatch(
Array.from({ length: 33 }, (_value, index) => field({ fieldId: `field:conversion-${index}` })),
), { code: "GN_FIELD_BUDGET_EXCEEDED" });
});
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 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/grease-pencil-marquee.ts");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "grease-pencil-marquee-unit-"));
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 modulePath = path.join(temporary, "grease-pencil-marquee.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const marquee = await import(pathToFileURL(modulePath));
const drawing = {
dataId: "grease-pencil:Data",
layerId: "grease-pencil-layer:Data:Lines",
frame: 12,
drawingId: "grease-pencil-drawing:Data:4",
};
function point(strokeIndex, pointIndex, viewportPosition) {
return {
...drawing,
strokeId: `grease-pencil-stroke:Data:4:${strokeIndex}`,
pointId: `grease-pencil-point:Data:4:${strokeIndex}:${pointIndex}`,
strokeIndex,
pointIndex,
viewportPosition,
};
}
function request(candidates) {
return {
schemaVersion: 1,
baseRevision: 27,
baseSelectionRevision: 4,
drawing,
box: { left: 0.2, top: 0.2, right: 0.8, bottom: 0.8 },
candidates,
};
}
test("M9-06 selects stable point and stroke IDs only inside the current drawing marquee", () => {
const result = marquee.selectGreasePencilMarquee(request([
point(1, 1, [0.5, 0.5]),
point(0, 2, [0.8, 0.2]),
point(0, 0, [0.1, 0.5]),
]), 27);
assert.deepEqual(result.drawing, drawing);
assert.equal(result.baseSelectionRevision, 4);
assert.deepEqual(result.selectedStrokeIds, [
"grease-pencil-stroke:Data:4:0",
"grease-pencil-stroke:Data:4:1",
]);
assert.deepEqual(result.selectedPoints.map(({ pointId, strokeId, strokeIndex, pointIndex }) => ({ pointId, strokeId, strokeIndex, pointIndex })), [
{ pointId: "grease-pencil-point:Data:4:0:2", strokeId: "grease-pencil-stroke:Data:4:0", strokeIndex: 0, pointIndex: 2 },
{ pointId: "grease-pencil-point:Data:4:1:1", strokeId: "grease-pencil-stroke:Data:4:1", strokeIndex: 1, pointIndex: 1 },
]);
});
test("M9-06 rejects stale, foreign-drawing, duplicate and forged marquee candidates", () => {
assert.throws(() => marquee.selectGreasePencilMarquee(request([point(0, 0, [0.5, 0.5])]), 28), (error) => error.code === "REVISION_CONFLICT");
assert.throws(() => marquee.selectGreasePencilMarquee(request([{ ...point(0, 0, [0.5, 0.5]), drawingId: "grease-pencil-drawing:Data:5" }]), 27), (error) => error.code === "GREASE_PENCIL_SELECTION_SCOPE_INVALID");
const duplicate = point(0, 0, [0.5, 0.5]);
assert.throws(() => marquee.selectGreasePencilMarquee(request([duplicate, { ...duplicate }]), 27), (error) => error.code === "GREASE_PENCIL_SELECTION_INVALID");
assert.throws(() => marquee.selectGreasePencilMarquee(request([{ ...point(0, 0, [0.5, 0.5]), pointId: "point:0" }]), 27), (error) => error.code === "GREASE_PENCIL_SELECTION_INVALID");
assert.throws(() => marquee.selectGreasePencilMarquee({ ...request([]), box: { left: 0.8, top: 0.2, right: 0.2, bottom: 0.8 } }, 27), (error) => error.code === "GREASE_PENCIL_SELECTION_INVALID");
});
test("M9-06 rejects a marquee candidate list over the bounded drawing point budget", () => {
const candidates = new Array(marquee.GREASE_PENCIL_MARQUEE_BUDGET.maxCandidates + 1).fill(null);
assert.throws(() => marquee.selectGreasePencilMarquee(request(candidates), 27), (error) => error.code === "GREASE_PENCIL_BUDGET_EXCEEDED");
});

View File

@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
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/grease-pencil-reorder.ts");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "grease-pencil-reorder-unit-"));
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 modulePath = path.join(temporary, "grease-pencil-reorder.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const reorder = await import(pathToFileURL(modulePath));
const dataId = "grease-pencil:Data";
const linesId = "grease-pencil-layer:Data:Lines";
const draftsId = "grease-pencil-layer:Data:Web Drafts";
const drawingId = "grease-pencil-drawing:Data:4";
const drawing = { id: drawingId, strokeCount: 0, pointCount: 0, strokes: [] };
const data = [{
id: dataId,
name: "Data",
geometryStatus: "available",
layerCount: 2,
frameCount: 2,
strokeCount: 0,
pointCount: 0,
layers: [
{ id: linesId, name: "Lines", visible: true, locked: false, opacity: 1, frames: [{ frame: 1, drawing: { ...drawing, id: "grease-pencil-drawing:Data:0" } }] },
{ id: draftsId, name: "Web Drafts", visible: true, locked: false, opacity: 1, frames: [{ frame: 1, drawing }] },
],
}];
test("M9-08 binds layer and frame reorder commands to stable IDs and Main revision", () => {
const layer = reorder.validateGreasePencilReorderCommand({
type: "moveGreasePencilLayer",
schemaVersion: 1,
dataId,
layerId: draftsId,
direction: "DOWN",
baseRevision: 27,
}, 27, data);
assert.deepEqual(layer, {
type: "moveGreasePencilLayer",
schemaVersion: 1,
dataId,
layerId: draftsId,
direction: "DOWN",
baseRevision: 27,
});
const frame = reorder.validateGreasePencilReorderCommand({
type: "moveGreasePencilFrame",
schemaVersion: 1,
dataId,
layerId: draftsId,
frame: 1,
targetFrame: 12,
drawingId,
baseRevision: 27,
}, 27, data);
assert.equal(frame.drawingId, drawingId);
assert.equal(frame.targetFrame, 12);
});
test("M9-08 rejects stale, no-op, forged drawing and occupied-target reorders", () => {
const layerCommand = { type: "moveGreasePencilLayer", schemaVersion: 1, dataId, layerId: draftsId, direction: "DOWN", baseRevision: 27 };
assert.throws(() => reorder.validateGreasePencilReorderCommand(layerCommand, 28, data), (error) => error.code === "REVISION_CONFLICT");
assert.throws(() => reorder.validateGreasePencilReorderCommand({ ...layerCommand, layerId: linesId }, 27, data), (error) => error.code === "GREASE_PENCIL_SCHEMA_INVALID");
const frameCommand = { type: "moveGreasePencilFrame", schemaVersion: 1, dataId, layerId: draftsId, frame: 1, targetFrame: 12, drawingId, baseRevision: 27 };
assert.throws(() => reorder.validateGreasePencilReorderCommand({ ...frameCommand, drawingId: "grease-pencil-drawing:Data:5" }, 27, data), (error) => error.code === "GREASE_PENCIL_SCHEMA_INVALID");
assert.throws(() => reorder.validateGreasePencilReorderCommand({ ...frameCommand, targetFrame: 1 }, 27, data), (error) => error.code === "GREASE_PENCIL_SCHEMA_INVALID");
const occupied = [{ ...data[0], layers: data[0].layers.map((layer) => layer.id === draftsId ? { ...layer, frames: [...layer.frames, { frame: 12, drawing: { ...drawing, id: "grease-pencil-drawing:Data:5" } }] } : layer) }];
assert.throws(() => reorder.validateGreasePencilReorderCommand(frameCommand, 27, occupied), (error) => error.code === "GREASE_PENCIL_SCHEMA_INVALID");
});

View File

@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
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/grease-pencil-selection.ts");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "grease-pencil-selection-unit-"));
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 modulePath = path.join(temporary, "grease-pencil-selection.mjs");
fs.writeFileSync(modulePath, transpiled.outputText);
const selection = await import(pathToFileURL(modulePath));
const drawing = {
dataId: "grease-pencil:Data",
layerId: "grease-pencil-layer:Data:Lines",
frame: 12,
drawingId: "grease-pencil-drawing:Data:4",
};
function point(strokeIndex, pointIndex) {
return {
...drawing,
strokeId: `grease-pencil-stroke:Data:4:${strokeIndex}`,
pointId: `grease-pencil-point:Data:4:${strokeIndex}:${pointIndex}`,
strokeIndex,
pointIndex,
};
}
function edit(baseSelectionRevision, source, operation, points) {
return { schemaVersion: 1, baseSelectionRevision, source, operation, points };
}
test("M9-07 serializes 2D canvas and 3D viewport edits through one selection revision", () => {
const initial = selection.createGreasePencilSelectionState(drawing);
const canvas = selection.applyGreasePencilSelectionEdit(initial, edit(0, "CANVAS_2D", "REPLACE", [point(0, 1)]), drawing);
assert.equal(canvas.revision, 1);
assert.equal(canvas.lastSource, "CANVAS_2D");
assert.deepEqual(canvas.selectedPoints.map((item) => item.pointId), [point(0, 1).pointId]);
const viewport = selection.applyGreasePencilSelectionEdit(canvas, edit(1, "VIEWPORT_3D", "ADD", [point(0, 0), point(1, 0)]), drawing);
assert.equal(viewport.revision, 2);
assert.equal(viewport.lastSource, "VIEWPORT_3D");
assert.deepEqual(viewport.selectedPoints.map((item) => item.pointId), [point(0, 0).pointId, point(0, 1).pointId, point(1, 0).pointId]);
const toggled = selection.applyGreasePencilSelectionEdit(viewport, edit(2, "CANVAS_2D", "TOGGLE", [point(0, 1)]), drawing);
assert.equal(toggled.revision, 3);
assert.deepEqual(toggled.selectedPoints.map((item) => item.pointId), [point(0, 0).pointId, point(1, 0).pointId]);
});
test("M9-07 rejects stale, foreign, duplicate and index-aliased selection edits", () => {
const initial = selection.createGreasePencilSelectionState(drawing);
assert.throws(
() => selection.applyGreasePencilSelectionEdit(initial, edit(1, "VIEWPORT_3D", "REPLACE", [point(0, 0)]), drawing),
(error) => error.code === "REVISION_CONFLICT",
);
assert.throws(
() => selection.applyGreasePencilSelectionEdit(initial, edit(0, "CANVAS_2D", "REPLACE", [{ ...point(0, 0), drawingId: "grease-pencil-drawing:Data:5" }]), drawing),
(error) => error.code === "GREASE_PENCIL_SELECTION_SCOPE_INVALID",
);
assert.throws(
() => selection.applyGreasePencilSelectionEdit(initial, edit(0, "CANVAS_2D", "REPLACE", [point(0, 0), point(0, 0)]), drawing),
(error) => error.code === "GREASE_PENCIL_SELECTION_INVALID",
);
assert.throws(
() => selection.applyGreasePencilSelectionEdit(initial, edit(0, "CANVAS_2D", "REPLACE", [point(0, 0), { ...point(0, 1), pointIndex: 0 }]), drawing),
(error) => error.code === "GREASE_PENCIL_SELECTION_INVALID",
);
});

View File

@@ -0,0 +1,41 @@
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/nanovdb-device-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);
test("M8-15 deterministically bounds visible-page replay by resident capacity", () => {
assert.deepEqual(recovery.planNanoVDBDeviceLossReplay([3, 1, 3, 2], 4, 2), {
schemaVersion: 1,
visiblePageIds: [1, 2, 3],
replayedPageIds: [1, 2],
skippedPageIds: [3],
pageCount: 4,
residentPageCapacity: 2,
});
});
test("M8-15 rejects invalid replay bounds and visible page IDs", () => {
for (const args of [
[[0], 0, 1],
[[0], 8193, 1],
[[0], 2, 0],
[[0], 2, 3],
[[-1], 2, 1],
[[2], 2, 1],
[[0.5], 2, 1],
]) {
assert.throws(() => recovery.planNanoVDBDeviceLossReplay(...args), /NANOVDB_INVALID_ARGUMENT/);
}
});

View File

@@ -0,0 +1,182 @@
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/nanovdb-page-feedback.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 feedback = await import(moduleUrl);
test("M8-01 freezes the bounded GPU feedback word layout", () => {
const buffer = feedback.createNanoVDBPageFeedbackBuffer();
const words = new Uint32Array(buffer);
assert.equal(buffer.byteLength, (feedback.NANOVDB_PAGE_FEEDBACK_HEADER_WORDS + feedback.NANOVDB_PAGE_FEEDBACK_DEFAULT_CAPACITY) * 4);
assert.deepEqual([...words.slice(0, 4)], [1, 1024, 0, 0]);
assert.ok([...words.slice(4)].every((word) => word === 0xffffffff));
assert.deepEqual(feedback.NANOVDB_PAGE_FEEDBACK_WORD, { schemaVersion: 0, capacity: 1, count: 2, overflow: 3, pageIds: 4 });
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_WGSL, /count: atomic<u32>/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_WGSL, /overflow: atomic<u32>/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_WGSL, /page_ids: array<atomic<u32>>/);
});
test("M8-02 records unique page IDs with a physical-capacity bound", () => {
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /arrayLength\(&nanovdb_page_feedback\.page_ids\)/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /min\(nanovdb_page_feedback\.capacity, physical_capacity\)/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /slot < capacity/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /atomicCompareExchangeWeak/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /claim\.old_value == page_id/);
assert.match(feedback.NANOVDB_PAGE_FEEDBACK_RECORD_WGSL, /atomicStore\(&nanovdb_page_feedback\.overflow, 1u\)/);
});
test("M8-03 sorts, deduplicates and binds CPU feedback to a render revision", () => {
const buffer = feedback.createNanoVDBPageFeedbackBuffer(4);
const words = new Uint32Array(buffer);
words[feedback.NANOVDB_PAGE_FEEDBACK_WORD.count] = 4;
words.set([7, 2, 7, 5], feedback.NANOVDB_PAGE_FEEDBACK_WORD.pageIds);
assert.deepEqual(feedback.parseNanoVDBPageFeedbackBatch(buffer, 8, 42), {
schemaVersion: 1,
renderRevision: 42,
attemptedCount: 4,
gpuStoredCount: 4,
uniqueCount: 3,
pageIds: [2, 5, 7],
status: "READY",
errorCode: null,
});
assert.throws(
() => feedback.parseNanoVDBPageFeedbackBatch(buffer, 8, -1),
(error) => error.code === "INVALID_ARGUMENT",
);
assert.throws(
() => feedback.parseNanoVDBPageFeedbackBatch(buffer, 8, 1.5),
(error) => error.code === "INVALID_ARGUMENT",
);
});
test("M8-04 rejects stale frame feedback before page I/O", async () => {
const buffer = feedback.createNanoVDBPageFeedbackBuffer(4);
const words = new Uint32Array(buffer);
words[feedback.NANOVDB_PAGE_FEEDBACK_WORD.count] = 3;
words.set([7, 2, 5], feedback.NANOVDB_PAGE_FEEDBACK_WORD.pageIds);
const batch = feedback.parseNanoVDBPageFeedbackBatch(buffer, 8, 42);
const requests = [];
const requestPage = async (pageId, renderRevision) => {
requests.push({ pageId, renderRevision });
};
assert.deepEqual(await feedback.dispatchNanoVDBPageFeedbackBatch(batch, 43, requestPage), {
schemaVersion: 1,
renderRevision: 42,
currentRenderRevision: 43,
status: "STALE",
requestedPageIds: [],
requestedCount: 0,
errorCode: "REVISION_CONFLICT",
});
assert.deepEqual(await feedback.dispatchNanoVDBPageFeedbackBatch(batch, 41, requestPage), {
schemaVersion: 1,
renderRevision: 42,
currentRenderRevision: 41,
status: "STALE",
requestedPageIds: [],
requestedCount: 0,
errorCode: "REVISION_CONFLICT",
});
assert.deepEqual(requests, []);
assert.deepEqual(await feedback.dispatchNanoVDBPageFeedbackBatch(batch, 42, requestPage), {
schemaVersion: 1,
renderRevision: 42,
currentRenderRevision: 42,
status: "ACCEPTED",
requestedPageIds: [2, 5, 7],
requestedCount: 3,
errorCode: null,
});
assert.deepEqual(requests, [
{ pageId: 2, renderRevision: 42 },
{ pageId: 5, renderRevision: 42 },
{ pageId: 7, renderRevision: 42 },
]);
await assert.rejects(
feedback.dispatchNanoVDBPageFeedbackBatch(batch, -1, requestPage),
(error) => error.code === "INVALID_ARGUMENT",
);
});
test("M8-01 parses ready and overflow feedback with a stable overflow code", () => {
const readyBuffer = feedback.createNanoVDBPageFeedbackBuffer(4);
const readyWords = new Uint32Array(readyBuffer);
readyWords[feedback.NANOVDB_PAGE_FEEDBACK_WORD.count] = 3;
readyWords.set([7, 2, 5], feedback.NANOVDB_PAGE_FEEDBACK_WORD.pageIds);
assert.deepEqual(feedback.parseNanoVDBPageFeedbackBuffer(readyBuffer, 8), {
schemaVersion: 1,
capacity: 4,
attemptedCount: 3,
storedCount: 3,
pageIds: [7, 2, 5],
status: "READY",
errorCode: null,
});
const overflowBuffer = feedback.createNanoVDBPageFeedbackBuffer(2);
const overflowWords = new Uint32Array(overflowBuffer);
overflowWords[feedback.NANOVDB_PAGE_FEEDBACK_WORD.count] = 4;
overflowWords[feedback.NANOVDB_PAGE_FEEDBACK_WORD.overflow] = 1;
overflowWords.set([3, 4], feedback.NANOVDB_PAGE_FEEDBACK_WORD.pageIds);
assert.deepEqual(feedback.parseNanoVDBPageFeedbackBuffer(overflowBuffer, 8), {
schemaVersion: 1,
capacity: 2,
attemptedCount: 4,
storedCount: 2,
pageIds: [3, 4],
status: "OVERFLOW",
errorCode: "NANOVDB_PAGE_FEEDBACK_OVERFLOW",
});
});
test("M8-01 rejects layout drift, invalid pages and inconsistent overflow", () => {
assert.throws(() => feedback.createNanoVDBPageFeedbackBuffer(0), (error) => error.code === "INVALID_ARGUMENT");
assert.throws(() => feedback.createNanoVDBPageFeedbackBuffer(8193), (error) => error.code === "INVALID_ARGUMENT");
const wrongSchema = feedback.createNanoVDBPageFeedbackBuffer(2);
new Uint32Array(wrongSchema)[0] = 2;
assert.throws(() => feedback.parseNanoVDBPageFeedbackBuffer(wrongSchema, 8), (error) => error.code === "PROTOCOL_MISMATCH");
const wrongCapacity = feedback.createNanoVDBPageFeedbackBuffer(2);
new Uint32Array(wrongCapacity)[1] = 8193;
assert.throws(() => feedback.parseNanoVDBPageFeedbackBuffer(wrongCapacity, 8), (error) => error.code === "PROTOCOL_MISMATCH");
const wrongSize = feedback.createNanoVDBPageFeedbackBuffer(2).slice(0, 20);
assert.throws(() => feedback.parseNanoVDBPageFeedbackBuffer(wrongSize, 8), (error) => error.code === "PROTOCOL_MISMATCH");
const inconsistent = feedback.createNanoVDBPageFeedbackBuffer(2);
const inconsistentWords = new Uint32Array(inconsistent);
inconsistentWords[2] = 3;
inconsistentWords.set([1, 2], 4);
assert.throws(() => feedback.parseNanoVDBPageFeedbackBuffer(inconsistent, 8), (error) => error.code === "PROTOCOL_MISMATCH");
const invalidPage = feedback.createNanoVDBPageFeedbackBuffer(2);
const invalidPageWords = new Uint32Array(invalidPage);
invalidPageWords[2] = 1;
invalidPageWords[4] = 8;
assert.throws(() => feedback.parseNanoVDBPageFeedbackBuffer(invalidPage, 8), (error) => error.code === "PROTOCOL_MISMATCH");
});
test("M8-01 reset clears atomic words and stale page IDs", () => {
const buffer = feedback.createNanoVDBPageFeedbackBuffer(3);
const words = new Uint32Array(buffer);
words[2] = 5;
words[3] = 1;
words.set([1, 2, 3], 4);
feedback.resetNanoVDBPageFeedbackBuffer(buffer);
assert.deepEqual([...words], [1, 3, 0, 0, 0xffffffff, 0xffffffff, 0xffffffff]);
});

View File

@@ -0,0 +1,47 @@
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/nanovdb-progressive-redraw.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 redraw = await import(moduleUrl);
test("M8-10 validates a bounded progressive redraw budget", () => {
assert.equal(redraw.NANOVDB_PROGRESSIVE_REDRAW_MAX_FRAMES, 32);
assert.equal(redraw.validateNanoVDBProgressiveRedrawLimit(1), 1);
assert.equal(redraw.validateNanoVDBProgressiveRedrawLimit(1024), 1024);
for (const value of [0, 1.5, 1025, Number.POSITIVE_INFINITY]) {
assert.throws(() => redraw.validateNanoVDBProgressiveRedrawLimit(value), /NANOVDB_INVALID_ARGUMENT/);
}
});
test("M8-10 caps repeated successful uploads with a stable error code", () => {
assert.deepEqual(redraw.consumeNanoVDBProgressiveRedrawBudget(0, 2), {
allowed: true,
redrawCount: 1,
capped: false,
errorCode: null,
});
assert.deepEqual(redraw.consumeNanoVDBProgressiveRedrawBudget(1, 2), {
allowed: true,
redrawCount: 2,
capped: true,
errorCode: "NANOVDB_PROGRESSIVE_REDRAW_LIMIT",
});
assert.deepEqual(redraw.consumeNanoVDBProgressiveRedrawBudget(2, 2), {
allowed: false,
redrawCount: 2,
capped: true,
errorCode: "NANOVDB_PROGRESSIVE_REDRAW_LIMIT",
});
assert.throws(() => redraw.consumeNanoVDBProgressiveRedrawBudget(-1, 2), /NANOVDB_INVALID_ARGUMENT/);
});

View File

@@ -0,0 +1,52 @@
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/nanovdb-render-golden.ts");
const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const golden = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const thresholds = { maxChannelError: 2, meanAbsoluteError: 0.5, rmsError: 1, alphaCoverageDeltaRatio: 0.25 };
test("M8-19 accepts a bounded RGBA8 desktop/WebGPU difference", () => {
const reference = Uint8Array.from([0, 10, 20, 0, 100, 110, 120, 255]);
const actual = Uint8Array.from([0, 11, 18, 0, 101, 110, 120, 255]);
assert.deepEqual(golden.compareNanoVDBRenderGolden(reference, actual, thresholds), {
schemaVersion: 1,
status: "READY",
pixelCount: 2,
comparedChannels: 8,
maxChannelError: 2,
meanAbsoluteError: 0.5,
rmsError: Math.sqrt(6 / 8),
referenceAlphaPixels: 1,
actualAlphaPixels: 1,
alphaCoverageDeltaRatio: 0,
thresholds,
errorCode: null,
});
});
test("M8-19 blocks channel and alpha coverage drift with a stable code", () => {
const reference = Uint8Array.from([0, 0, 0, 0, 10, 10, 10, 255]);
const actual = Uint8Array.from([9, 0, 0, 255, 10, 10, 10, 255]);
const result = golden.compareNanoVDBRenderGolden(reference, actual, thresholds);
assert.equal(result.status, "BLOCKED");
assert.equal(result.errorCode, "NANOVDB_GOLDEN_MISMATCH");
assert.equal(result.maxChannelError, 255);
assert.equal(result.alphaCoverageDeltaRatio, 0.5);
for (const args of [
[new Uint8Array(), new Uint8Array(), thresholds],
[new Uint8Array(4), new Uint8Array(8), thresholds],
[new Uint8Array(3), new Uint8Array(3), thresholds],
[new Uint8Array(4), new Uint8Array(4), { ...thresholds, rmsError: -1 }],
]) assert.throws(() => golden.compareNanoVDBRenderGolden(...args), /NANOVDB_INVALID_ARGUMENT/);
});

View File

@@ -0,0 +1,91 @@
import assert from "node:assert/strict";
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(), "nla-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(root, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("nla.ts", "nla.mjs", [['from "./capability-gates"', 'from "./capability-gates.mjs"']]);
const nla = await import(pathToFileURL(path.join(temporary, "nla.mjs")));
const actionId = "action:Move:object:Cube";
const context = {
actionIds: new Set([actionId]),
actionChannelPaths: new Map([[actionId, new Set(["location[0]"])]]),
ownerId: "object:Cube",
};
function tracks() {
return [{
schemaVersion: 1,
id: "track:Move",
ownerId: "object:Cube",
name: "Move",
muted: false,
solo: false,
selected: true,
strips: [
{ id: "strip:A", actionId, frameStart: 20, frameEnd: 40, actionFrameStart: 1, actionFrameEnd: 11, scale: 2, repeat: 1, blendIn: 0, blendOut: 0, influence: 1, blendMode: "REPLACE", extrapolation: "NOTHING", muted: false, selected: true, stripType: "CLIP" },
{ id: "strip:B", actionId, frameStart: 45, frameEnd: 65, actionFrameStart: 1, actionFrameEnd: 11, scale: 1, repeat: 2, blendIn: 0, blendOut: 0, influence: 1, blendMode: "REPLACE", extrapolation: "NOTHING", muted: false, selected: true, stripType: "CLIP" },
],
}];
}
test("M10-12 moves one NLA strip without mutating the source stack", () => {
const source = tracks();
const before = structuredClone(source);
const moved = nla.moveNlaStrip(source, {
type: "moveNLAStrip",
objectId: "object:Cube",
trackId: "track:Move",
stripId: "strip:A",
frameStart: 5,
baseRevision: 7,
}, context);
assert.deepEqual(source, before);
assert.deepEqual(moved[0].strips.map((strip) => [strip.id, strip.frameStart, strip.frameEnd]), [
["strip:A", 5, 25],
["strip:B", 45, 65],
]);
});
test("M10-12 rejects overlap, unknown identities and invalid operator budgets", () => {
const command = { type: "moveNLAStrip", objectId: "object:Cube", trackId: "track:Move", stripId: "strip:A", frameStart: 30, baseRevision: 7 };
assert.throws(() => nla.moveNlaStrip(tracks(), command, context), { code: "NLA_INVALID_STACK" });
assert.throws(() => nla.moveNlaStrip(tracks(), { ...command, trackId: "track:missing" }, context), { code: "NLA_INVALID_STACK" });
assert.throws(() => nla.moveNlaStrip(tracks(), { ...command, frameStart: 1_000_001 }, context), { code: "NLA_INVALID_STACK" });
});
test("M10-15 rejects NLA topology that exceeds the browser memory budget", () => {
const oversized = new Array(nla.NLA_STACK_BUDGET.maxTracks + 1).fill(tracks()[0]);
assert.throws(() => nla.parseNlaTracks(oversized), { code: "NLA_BUDGET_EXCEEDED" });
const tooManyStrips = [{
...tracks()[0],
strips: new Array(nla.NLA_STACK_BUDGET.maxStripsPerTrack + 1).fill(tracks()[0].strips[0]),
}];
assert.throws(() => nla.parseNlaTracks(tooManyStrips), { code: "NLA_BUDGET_EXCEEDED" });
});
test("M10-15 rejects undeclared NLA input and recovers on the next valid stack", () => {
const malicious = tracks();
malicious[0].strips[0].proxySuccess = true;
assert.throws(() => nla.parseNlaTracks(malicious), { code: "NLA_INVALID_STACK" });
assert.equal(nla.gateNlaTracks(tracks(), context).status, "READY");
});

View File

@@ -0,0 +1,79 @@
import assert from "node:assert/strict";
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(), "paint-depth-visibility-unit-"));
function transpile(sourceName, outputName, transform = (source) => source) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
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, outputName), transform(transpiled.outputText));
}
transpile("paint.ts", "paint.mjs");
transpile("paint-depth-visibility.ts", "paint-depth-visibility.mjs", (source) => source.replace('from "./paint"', 'from "./paint.mjs"'));
const visibility = await import(pathToFileURL(path.join(temporary, "paint-depth-visibility.mjs")));
const request = {
schemaVersion: 1,
objectId: "object:Paint",
meshId: "mesh:Paint",
revision: 7,
vertexIndices: [6, 2, 4],
};
test("M9-09 binds GPU depth visibility to stable vertex identities and Main revision", () => {
const parsed = visibility.validatePaintDepthVisibilityRequest(request, 7);
assert.deepEqual(parsed.vertexIndices, [2, 4, 6]);
const result = visibility.validatePaintDepthVisibilityResult({
...parsed,
backend: "MAIN_THREAD_WEBGL2",
source: "GPU_RGBA_DEPTH_READBACK",
width: 64,
height: 32,
depthReadbackBytes: 64 * 32 * 4,
occluderPixelCount: 512,
visibleVertexIndices: [2, 6],
}, parsed);
assert.deepEqual(result.visibleVertexIndices, [2, 6]);
assert.equal(result.source, "GPU_RGBA_DEPTH_READBACK");
});
test("M9-09 fails closed on stale, forged, duplicate and over-budget depth samples", () => {
assert.throws(() => visibility.validatePaintDepthVisibilityRequest(request, 8), (error) => error.code === "REVISION_CONFLICT");
assert.throws(() => visibility.validatePaintDepthVisibilityRequest({ ...request, objectId: "mesh:Paint" }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.throws(() => visibility.validatePaintDepthVisibilityRequest({ ...request, vertexIndices: [2, 2] }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.throws(() => visibility.validatePaintDepthVisibilityRequest({ ...request, undeclared: true }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.throws(
() => visibility.validatePaintDepthVisibilityRequest({ ...request, vertexIndices: Array.from({ length: 100_001 }, (_, index) => index) }, 7),
(error) => error.code === "PAINT_BUDGET_EXCEEDED",
);
});
test("M9-09 rejects result drift and accepts a verified empty visible set", () => {
const parsed = visibility.validatePaintDepthVisibilityRequest(request, 7);
const base = {
...parsed,
backend: "OFFSCREEN_WEBGL2",
source: "GPU_RGBA_DEPTH_READBACK",
width: 32,
height: 32,
depthReadbackBytes: 4096,
occluderPixelCount: 128,
visibleVertexIndices: [],
};
assert.deepEqual(visibility.validatePaintDepthVisibilityResult(base, parsed).visibleVertexIndices, []);
assert.throws(() => visibility.validatePaintDepthVisibilityResult({ ...base, visibleVertexIndices: [99] }, parsed), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.throws(() => visibility.validatePaintDepthVisibilityResult({ ...base, depthReadbackBytes: 4095 }, parsed), (error) => error.code === "PAINT_BUDGET_EXCEEDED");
assert.throws(() => visibility.validatePaintDepthVisibilityResult({ ...base, source: "CPU_RAYCAST" }, parsed), (error) => error.code === "PAINT_SCHEMA_INVALID");
});

View File

@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
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(), "paint-pbvh-capability-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("paint-pbvh-capability.ts", "paint-pbvh-capability.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
]);
const pbvh = await import(pathToFileURL(path.join(temporary, "paint-pbvh-capability.mjs")));
const base = {
schemaVersion: 1,
operation: "PBVH_BRUSH",
domain: "VERTEX_COLOR",
brush: "DRAW",
objectId: "object:Paint",
meshId: "mesh:Paint",
baseRevision: 7,
};
test("M9-13 freezes the active Blender 5.2 PBVH-related brush inventory", () => {
const inventory = pbvh.paintPBVHBrushInventory();
const counts = Object.fromEntries(["SCULPT", "VERTEX_COLOR", "WEIGHT", "TEXTURE"].map((domain) => [
domain,
inventory.filter((entry) => entry.domain === domain).length,
]));
assert.deepEqual(counts, { SCULPT: 32, VERTEX_COLOR: 4, WEIGHT: 4, TEXTURE: 6 });
assert.equal(new Set(inventory.map((entry) => `${entry.domain}:${entry.brush}`)).size, 46);
assert.ok(inventory.every((entry) => entry.source.endsWith("DNA_brush_enums.h")));
});
test("M9-13 blocks every PBVH brush when the WASM entrypoint is absent", () => {
for (const entry of pbvh.paintPBVHBrushInventory()) {
const gate = pbvh.gatePaintPBVHCapability({ ...base, domain: entry.domain, brush: entry.brush }, {
nativeEntrypointPresent: false,
sessionContextReady: false,
verifiedBrushes: new Set(),
currentRevision: 7,
currentObjectId: base.objectId,
currentMeshId: base.meshId,
});
assert.equal(gate.taskId, "N-017");
assert.equal(gate.status, "BLOCKED");
assert.deepEqual(gate.issues.map((issue) => issue.code), ["PAINT_PBVH_UNAVAILABLE"]);
assert.equal(gate.issues[0].recoverable, false);
}
});
test("M9-13 remains fail-closed after symbol discovery until context and a brush golden exist", () => {
const common = { nativeEntrypointPresent: true, verifiedBrushes: new Set(), currentRevision: 7 };
assert.equal(pbvh.gatePaintPBVHCapability(base, { ...common, sessionContextReady: false }).issues[0].code, "PAINT_PBVH_CONTEXT_UNAVAILABLE");
assert.equal(pbvh.gatePaintPBVHCapability(base, { ...common, sessionContextReady: true }).issues[0].code, "PAINT_PBVH_BRUSH_UNVERIFIED");
assert.equal(pbvh.gatePaintPBVHCapability(base, {
...common,
sessionContextReady: true,
verifiedBrushes: new Set(["VERTEX_COLOR:DRAW"]),
}).status, "READY");
});
test("M9-13 validates identity and revision before reporting runtime availability", () => {
const context = {
nativeEntrypointPresent: false,
sessionContextReady: false,
verifiedBrushes: new Set(),
currentRevision: 7,
currentObjectId: base.objectId,
currentMeshId: base.meshId,
};
assert.equal(pbvh.gatePaintPBVHCapability({ ...base, baseRevision: 6 }, context).issues[0].code, "REVISION_CONFLICT");
assert.equal(pbvh.gatePaintPBVHCapability({ ...base, objectId: "object:Other" }, context).issues[0].code, "PAINT_SCHEMA_INVALID");
assert.throws(() => pbvh.parsePaintPBVHCapabilityRequest({ ...base, brush: "FAKE" }), { code: "PAINT_PBVH_BRUSH_UNVERIFIED" });
assert.throws(() => pbvh.parsePaintPBVHCapabilityRequest({ ...base, proxySuccess: true }), { code: "PAINT_SCHEMA_INVALID" });
});

View File

@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
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(), "paint-stroke-session-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replace(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("paint.ts", "paint.mjs");
transpile("paint-stroke-session.ts", "paint-stroke-session.mjs", [['from "./paint"', 'from "./paint.mjs"']]);
const sessions = await import(pathToFileURL(path.join(temporary, "paint-stroke-session.mjs")));
const begin = {
schemaVersion: 1,
pointerSessionId: "paint-pointer:unit-1",
baseRevision: 7,
target: { mode: "VERTEX_COLOR", meshId: "mesh:Paint", attributeName: "StrokeColor", domain: "POINT" },
};
test("M9-10 merges ordered pointer chunks into one deterministic Main command", () => {
const store = new sessions.PaintStrokeSessionStore();
assert.equal(store.begin(begin, 7).state, "OPEN");
store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 0, indices: [3, 1], values: [1, 0, 0, 1, 0, 1, 0, 1] }, 7);
const buffered = store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 1, indices: [1, 2], values: [0, 0, 1, 1, 1, 1, 0, 1] }, 7);
assert.deepEqual({ chunks: buffered.chunkCount, received: buffered.receivedEntryCount, unique: buffered.uniqueEntryCount, bytes: buffered.bufferedBytes }, { chunks: 2, received: 4, unique: 3, bytes: 80 });
const committed = store.commit({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, expectedChunkCount: 2 }, 7);
assert.deepEqual(committed.command, {
type: "setVertexColors",
meshId: "mesh:Paint",
attributeName: "StrokeColor",
domain: "POINT",
indices: [1, 2, 3],
colors: [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1],
});
assert.equal(committed.receipt.state, "READY");
assert.equal(store.activeCount, 0);
});
test("M9-10 cancels without a Main command and rejects stale or discontinuous chunks", () => {
const store = new sessions.PaintStrokeSessionStore();
store.begin(begin, 7);
assert.throws(() => store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 1, indices: [0], values: [1, 1, 1, 1] }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.throws(() => store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 0, indices: [0], values: [1, 1, 1, 1] }, 8), (error) => error.code === "REVISION_CONFLICT");
const cancelled = store.cancel({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7 });
assert.equal(cancelled.state, "CANCELLED");
assert.equal(store.activeCount, 0);
});
test("M9-10 merges weight chunks with their normalize policy into one Main command", () => {
const store = new sessions.PaintStrokeSessionStore();
const weightBegin = {
schemaVersion: 1,
pointerSessionId: "paint-pointer:weight-unit",
baseRevision: 9,
target: { mode: "WEIGHT", objectId: "object:Paint", vertexGroup: "StrokeWeight", normalize: true, mirror: false },
};
store.begin(weightBegin, 9);
store.append({ schemaVersion: 1, pointerSessionId: weightBegin.pointerSessionId, baseRevision: 9, chunkIndex: 0, indices: [4, 2], values: [0.25, 0.75] }, 9);
store.append({ schemaVersion: 1, pointerSessionId: weightBegin.pointerSessionId, baseRevision: 9, chunkIndex: 1, indices: [4], values: [0.5] }, 9);
const committed = store.commit({ schemaVersion: 1, pointerSessionId: weightBegin.pointerSessionId, baseRevision: 9, expectedChunkCount: 2 }, 9);
assert.deepEqual(committed.command, {
type: "setVertexWeights",
objectId: "object:Paint",
vertexGroup: "StrokeWeight",
indices: [2, 4],
values: [0.75, 0.5],
normalize: true,
mirror: false,
});
});
test("M9-10 bounds chunks, values and final chunk counts before Main", () => {
const store = new sessions.PaintStrokeSessionStore();
store.begin(begin, 7);
assert.throws(() => store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 0, indices: [0], values: [2, 0, 0, 1] }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
store.append({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, chunkIndex: 0, indices: [0], values: [1, 0, 0, 1] }, 7);
assert.throws(() => store.commit({ schemaVersion: 1, pointerSessionId: begin.pointerSessionId, baseRevision: 7, expectedChunkCount: 2 }, 7), (error) => error.code === "PAINT_SCHEMA_INVALID");
assert.equal(store.activeCount, 0);
});

View File

@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
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(), "physics-cache-family-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(root, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("physics-simulation.ts", "physics-simulation.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
]);
const physics = await import(pathToFileURL(path.join(temporary, "physics-simulation.mjs")));
const digest = async (value) => Array.from(
new Uint8Array(await crypto.subtle.digest("SHA-256", value)),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
async function cachedSystem(family, index = 0, cachePatch = {}) {
const source = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, index]).buffer;
const payload = Uint8Array.from([index + 1, 2, index + 3, 4]).buffer;
const first = payload.slice(0, 2);
const second = payload.slice(2, 4);
const settingsHash = await digest(Uint8Array.from([index + 11]).buffer);
const cache = {
schemaVersion: 1,
cacheKey: `physics-${family.toLowerCase()}-1-2`,
family,
source: index % 2 === 0 ? "BLENDER_DESKTOP_BAKE" : "BLENDER_SERVER_BAKE",
blenderVersion: "5.2.0",
sourceBlendSha256: await digest(source),
settingsHash,
inputHash: await digest(Uint8Array.from([index + 21]).buffer),
cacheSha256: await digest(payload),
frameStart: 1,
frameEnd: 2,
byteLength: payload.byteLength,
frames: [
{ frame: 1, byteOffset: 0, byteLength: 2, sha256: await digest(first) },
{ frame: 2, byteOffset: 2, byteLength: 2, sha256: await digest(second) },
],
status: "COMPLETE",
...cachePatch,
};
const manifest = {
schemaVersion: 1,
systems: [{
id: `physics:${family.toLowerCase()}`,
family,
ownerObjectId: `object:${family}`,
settingsHash,
settings: { enabled: true },
dependencyIds: [],
cache,
}],
};
return { manifest, source, payload };
}
test("M10-14 verifies source, payload and frame hashes for every Physics family", async () => {
for (const [index, family] of physics.PHYSICS_FAMILIES.entries()) {
const value = await cachedSystem(family, index);
const parsed = physics.parsePhysicsSimulationManifest(value.manifest);
const cache = await physics.verifyPhysicsCachePayload(parsed.systems[0], value.source, value.payload);
assert.equal(cache.family, family);
assert.equal(cache.source, index % 2 === 0 ? "BLENDER_DESKTOP_BAKE" : "BLENDER_SERVER_BAKE");
assert.equal(cache.frames.length, 2);
assert.deepEqual(physics.selectPhysicsCacheFrame(parsed.systems[0], 2), { cacheKey: cache.cacheKey, frame: 2 });
}
});
test("M10-14 rejects source and cache byte drift before playback", async () => {
const value = await cachedSystem("CLOTH");
const parsed = physics.parsePhysicsSimulationManifest(value.manifest);
await assert.rejects(
physics.verifyPhysicsCachePayload(parsed.systems[0], Uint8Array.from([1]).buffer, value.payload),
{ code: "PHYSICS_CACHE_SOURCE_MISMATCH" },
);
await assert.rejects(
physics.verifyPhysicsCachePayload(parsed.systems[0], value.source, Uint8Array.from([9, 9, 9, 9]).buffer),
{ code: "PHYSICS_CACHE_HASH_MISMATCH" },
);
});
test("M10-14 rejects family, version, range, byte budget and undeclared cache fields", async () => {
const value = await cachedSystem("FLUID");
const cache = value.manifest.systems[0].cache;
const invalid = [
[{ ...cache, schemaVersion: 2 }, "PROTOCOL_MISMATCH"],
[{ ...cache, blenderVersion: "5.3.0" }, "PROTOCOL_MISMATCH"],
[{ ...cache, family: "CLOTH" }, "PHYSICS_MANIFEST_INVALID"],
[{ ...cache, byteLength: physics.PHYSICS_SIMULATION_BUDGET.maxCacheBytes + 1 }, "PHYSICS_BUDGET_EXCEEDED"],
[{ ...cache, frames: [{ ...cache.frames[0], byteOffset: 1 }, cache.frames[1]] }, "PHYSICS_CACHE_FRAME_MISMATCH"],
[{ ...cache, frames: [cache.frames[0]] }, "PHYSICS_CACHE_FRAME_MISMATCH"],
[{ ...cache, proxySuccess: true }, "PHYSICS_MANIFEST_INVALID"],
];
for (const [candidate, code] of invalid) {
assert.throws(() => physics.parsePhysicsSimulationManifest({
...value.manifest,
systems: [{ ...value.manifest.systems[0], cache: candidate }],
}), { code });
}
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,98 @@
import assert from "node:assert/strict";
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(), "physics-solver-probe-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(root, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("physics-simulation.ts", "physics-simulation.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
]);
const physics = await import(pathToFileURL(path.join(temporary, "physics-simulation.mjs")));
const mib = 1024 * 1024;
test("M10-13 defaults every Physics family to desktop/server bake", async () => {
const capabilities = await physics.probePhysicsSolverCapabilities(undefined, {
threadMode: "PTHREAD",
memoryLimitBytes: 2_048 * mib,
});
assert.deepEqual(capabilities.map((entry) => entry.family), physics.PHYSICS_FAMILIES);
assert.ok(capabilities.every((entry) => entry.localSolver === "BLOCKED"));
assert.ok(capabilities.every((entry) => entry.solverProbe === "EXPORT_UNAVAILABLE"));
assert.ok(capabilities.every((entry) => entry.unsupportedRoute === "DESKTOP_SERVER_BAKE"));
});
test("M10-13 probes export, initialization, threads and memory per family", async () => {
const initialization = {
RIGID_BODY: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: 64 * mib },
SOFT_BODY: { initialized: false, requiredThreadMode: "SINGLE", requiredMemoryBytes: 64 * mib },
CLOTH: { initialized: true, requiredThreadMode: "PTHREAD", requiredMemoryBytes: 128 * mib },
FLUID: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: 512 * mib },
DYNAMIC_PAINT: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: Number.NaN },
};
const initialized = [];
const runtime = {
hasFamilyExport: (family) => family !== "HAIR",
initializeFamily: async (family) => {
initialized.push(family);
if (family === "PARTICLE") throw new Error("init failed");
return initialization[family];
},
};
const capabilities = await physics.probePhysicsSolverCapabilities(runtime, {
threadMode: "SINGLE",
memoryLimitBytes: 256 * mib,
});
assert.deepEqual(Object.fromEntries(capabilities.map((entry) => [entry.family, entry.solverProbe])), {
RIGID_BODY: "READY",
SOFT_BODY: "INITIALIZATION_FAILED",
CLOTH: "THREADS_UNAVAILABLE",
FLUID: "MEMORY_UNAVAILABLE",
DYNAMIC_PAINT: "INVALID_RESULT",
PARTICLE: "INITIALIZATION_FAILED",
HAIR: "EXPORT_UNAVAILABLE",
});
assert.deepEqual(initialized, ["RIGID_BODY", "SOFT_BODY", "CLOTH", "FLUID", "DYNAMIC_PAINT", "PARTICLE"]);
assert.deepEqual(physics.selectPhysicsExecutionRoute("RIGID_BODY", capabilities), {
family: "RIGID_BODY", mode: "LOCAL_SOLVER", probe: "READY",
});
assert.deepEqual(physics.selectPhysicsExecutionRoute("CLOTH", capabilities), {
family: "CLOTH", mode: "DESKTOP_SERVER_BAKE", probe: "THREADS_UNAVAILABLE",
});
assert.equal(physics.gatePhysicsExecution("RIGID_BODY", "LOCAL_SOLVER", capabilities).status, "READY");
const blocked = physics.gatePhysicsExecution("CLOTH", "LOCAL_SOLVER", capabilities);
assert.equal(blocked.status, "BLOCKED");
assert.equal(blocked.issues[0].code, "PHYSICS_SOLVER_UNAVAILABLE");
assert.match(blocked.issues[0].message, /desktop\/server bake/);
});
test("M10-13 fails closed on an invalid environment or forged probe result", async () => {
await assert.rejects(
physics.probePhysicsSolverCapabilities(undefined, { threadMode: "SINGLE", memoryLimitBytes: 0 }),
{ code: "PHYSICS_MANIFEST_INVALID" },
);
const capabilities = await physics.probePhysicsSolverCapabilities({
hasFamilyExport: () => true,
initializeFamily: () => ({ initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: -1 }),
}, { threadMode: "PTHREAD", memoryLimitBytes: 2_048 * mib });
assert.ok(capabilities.every((entry) => entry.solverProbe === "INVALID_RESULT"));
assert.ok(capabilities.every((entry) => entry.localSolver === "BLOCKED"));
});

View File

@@ -0,0 +1,80 @@
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/recent-projects.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 {
normalizeRecentProjects,
parseRecentProjectIndex,
removeRecentProject,
upsertRecentProject,
classifyRecentProjectIdentity,
} = await import(moduleUrl);
function project(projectId, overrides = {}) {
return {
schemaVersion: 1,
projectId,
displayName: `${projectId}.blend`,
revision: 7,
bytes: 1024,
sha256: (projectId.charCodeAt(0) % 16).toString(16).repeat(64),
updatedAt: "2026-08-15T12:00:00.000Z",
lastOpenedAt: "2026-08-15T12:00:00.000Z",
backend: "opfs",
...overrides,
};
}
test("M7-09 recent projects sort and deduplicate deterministically", () => {
const older = project("alpha", { revision: 2, updatedAt: "2026-08-15T08:00:00-04:00", lastOpenedAt: "2026-08-15T08:00:00-04:00" });
const newer = project("alpha", { revision: 3, updatedAt: "2026-08-15T12:01:00.000Z", lastOpenedAt: "2026-08-15T12:01:00.000Z" });
const second = project("beta", { lastOpenedAt: "2026-08-15T11:59:00.000Z" });
const forward = normalizeRecentProjects([older, second, newer]);
const reverse = normalizeRecentProjects([newer, second, older]);
assert.deepEqual(forward, reverse);
assert.deepEqual(forward.index.projects.map(({ projectId, revision }) => [projectId, revision]), [["alpha", 3], ["beta", 7]]);
assert.equal(forward.index.projects[0].updatedAt, "2026-08-15T12:01:00.000Z");
});
test("M7-09 malformed records are quarantined without hiding valid projects", () => {
const parsed = parseRecentProjectIndex({
schemaVersion: 1,
projects: [project("valid"), { ...project("bad"), sha256: "not-a-digest" }, { ...project("blank"), displayName: " " }],
});
assert.equal(parsed.quarantined, 2);
assert.deepEqual(parsed.index.projects.map((item) => item.projectId), ["valid"]);
assert.deepEqual(parseRecentProjectIndex({ schemaVersion: 99, projects: [] }), {
index: { schemaVersion: 1, projects: [] },
quarantined: 1,
});
});
test("M7-09 upsert and removal keep a bounded stable index", () => {
let index = { schemaVersion: 1, projects: [] };
index = upsertRecentProject(index, project("alpha"));
index = upsertRecentProject(index, project("alpha", { revision: 9, lastOpenedAt: "2026-08-15T12:02:00Z" }));
index = upsertRecentProject(index, project("beta"));
assert.deepEqual(index.projects.map(({ projectId, revision }) => [projectId, revision]), [["alpha", 9], ["beta", 7]]);
assert.deepEqual(removeRecentProject(index, "alpha").projects.map((item) => item.projectId), ["beta"]);
assert.equal(normalizeRecentProjects(index.projects, 0).index.projects.length, 0);
});
test("M7-10 recent project integrity classifies missing and mismatched content", () => {
const expected = { revision: 7, bytes: 1024, sha256: "a".repeat(64) };
assert.equal(classifyRecentProjectIdentity(expected, undefined), "MISSING");
assert.equal(classifyRecentProjectIdentity(expected, { ...expected, sha256: "b".repeat(64) }), "HASH_MISMATCH");
assert.equal(classifyRecentProjectIdentity(expected, { ...expected, revision: 8 }), "METADATA_MISMATCH");
assert.equal(classifyRecentProjectIdentity(expected, expected), undefined);
});

View File

@@ -0,0 +1,77 @@
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 root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/render-budget.ts");
const source = fs.readFileSync(sourcePath, "utf8")
.replace('import type { ErrorCode } from "./error";\n', "")
.replace('import { MAX_GPU_TEXTURE_ASSETS, MAX_GPU_TEXTURE_BYTES, MAX_GPU_TEXTURE_DIMENSION, type GPUTextureAsset } from "./render-assets";\n', "const MAX_GPU_TEXTURE_ASSETS = 256; const MAX_GPU_TEXTURE_BYTES = 64 * 1024 * 1024; const MAX_GPU_TEXTURE_DIMENSION = 16_384;\n")
.replace('import type { SceneSnapshotIR } from "./scene-ir";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const budget = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-03/render-resource-budget.json"), "utf8"));
function snapshot(lightCount) {
const lights = Array.from({ length: lightCount }, (_, index) => ({
id: `light:${index}`, lightType: 2, castsShadow: true,
}));
const nodes = lights.map((light, index) => ({
id: `object:light:${index}`, type: "LIGHT", visible: true, dataId: light.id,
}));
return { lights, nodes };
}
test("M11-03 freezes explicit Three WebGL2 and WebGPU product budgets", () => {
for (const [backend, expected] of [["THREE_WEBGL2", golden.webgl2], ["THREE_WEBGPU", golden.webgpu]]) {
const actual = budget.resolvePBRRenderBudget(backend);
for (const [field, value] of Object.entries(expected)) assert.equal(actual[field], value, `${backend}.${field}`);
}
const clamped = budget.resolvePBRRenderBudget("THREE_WEBGPU", {
maxLights: 32, maxShadowMaps: 4, maxShadowMapDimension: 1024, maxTextureDimension2D: 8192,
});
assert.deepEqual([clamped.maxLights, clamped.maxShadowMaps, clamped.shadowMapDimension, clamped.maxTextureDimension], [32, 4, 1024, 8192]);
assert.throws(() => budget.resolvePBRRenderBudget("THREE_WEBGPU", { maxLights: 0 }), /GPU_TEXTURE_BUDGET_EXCEEDED/);
});
test("M11-03 deterministically bounds lights and shadow maps", () => {
const report = budget.planPBRLightingBudget(snapshot(golden.overflow.requestedLights));
assert.equal(report.status, "BLOCKED");
assert.equal(report.requestedLights, golden.overflow.requestedLights);
assert.equal(report.renderedLightNodeIds.length, golden.overflow.renderedLights);
assert.equal(report.droppedLightNodeIds.length, golden.overflow.droppedLights);
assert.equal(report.requestedShadowMaps, golden.overflow.requestedShadowMaps);
assert.equal(report.shadowLightNodeIds.length, golden.overflow.renderedShadowMaps);
assert.equal(report.shadowBlockedLightNodeIds.length, golden.overflow.blockedShadowMaps);
assert.deepEqual(report.issues.map((issue) => issue.code), golden.overflow.codes);
assert.equal(report.renderedLightNodeIds[0], "object:light:0");
assert.equal(report.renderedLightNodeIds.at(-1), "object:light:13");
});
test("M11-03 rejects aggregate texture allocation before decode", () => {
const small = { assetId: "asset:small", imageId: "image:small", usage: "BASE_COLOR", width: 4, height: 4, byteLength: 16 };
assert.deepEqual(budget.planPBRTextureBudget([small]), {
schemaVersion: 1,
backend: "THREE_WEBGL2",
status: "READY",
budget: budget.resolvePBRRenderBudget("THREE_WEBGL2"),
requestedAssets: 1,
payloadBytes: 16,
decodedGPUBytes: 64,
maxRequestedDimension: 4,
issues: [],
});
const tooMany = Array.from({ length: 257 }, (_, index) => ({ ...small, assetId: `asset:${index}`, imageId: `image:${index}` }));
const report = budget.planPBRTextureBudget(tooMany);
assert.equal(report.status, "BLOCKED");
assert.equal(report.requestedAssets, 257);
assert.equal(report.issues[0].code, "GPU_TEXTURE_BUDGET_EXCEEDED");
assert.equal(budget.planPBRTextureBudget([{ ...small, width: 16_384, height: 16_384 }]).status, "BLOCKED");
});

View File

@@ -0,0 +1,65 @@
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 root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/render-image-comparison.ts");
const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const comparison = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const thresholds = {
maxMeanAbsoluteError: 8,
maxRootMeanSquaredError: 16,
maxP95ChannelError: 16,
maxBadPixelRatio: 0.1,
badPixelChannelError: 32,
foregroundDeltaFromReferenceBackground: 32,
minForegroundIntersectionOverUnion: 0.8,
maxAlphaCoverageDeltaRatio: 0,
};
function frame(width = 4, height = 4) {
const pixels = new Uint8Array(width * height * 4);
for (let index = 0; index < pixels.length; index += 4) pixels.set([64, 64, 64, 255], index);
for (const pixel of [5, 6, 9, 10]) pixels.set([0, 0, 0, 255], pixel * 4);
return pixels;
}
test("M11-04 reports every explainable metric for identical SRGB8 frames", () => {
const reference = frame();
const report = comparison.compareRenderImages(reference, reference.slice(), 4, 4, thresholds);
assert.equal(report.status, "READY");
assert.deepEqual(
[report.meanAbsoluteError, report.rootMeanSquaredError, report.p95ChannelError, report.badPixelRatio],
[0, 0, 0, 0],
);
assert.equal(report.foregroundIntersectionOverUnion, 1);
assert.equal(report.checks.length, 6);
assert.equal(report.errorCode, null);
});
test("M11-04 rejects a non-empty but compositionally wrong frame", () => {
const reference = frame();
const wrong = frame();
for (let index = 0; index < wrong.length; index += 4) wrong.set([64, 64, 64, 255], index);
wrong.set([255, 0, 0, 255], 0);
const report = comparison.compareRenderImages(reference, wrong, 4, 4, thresholds);
assert.equal(report.status, "BLOCKED");
assert.equal(report.errorCode, "RENDER_REFERENCE_MISMATCH");
assert.ok(report.foregroundIntersectionOverUnion < thresholds.minForegroundIntersectionOverUnion);
assert.ok(report.checks.some((item) => !item.passed));
});
test("M11-04 rejects invalid dimensions, byte lengths and thresholds", () => {
assert.throws(() => comparison.compareRenderImages(frame(), frame(), 0, 4, thresholds), /INVALID_ARGUMENT/);
assert.throws(() => comparison.compareRenderImages(frame(), frame().subarray(1), 4, 4, thresholds), /INVALID_ARGUMENT/);
assert.throws(() => comparison.compareRenderImages(frame(), frame(), 4, 4, { ...thresholds, maxBadPixelRatio: 2 }), /INVALID_ARGUMENT/);
});

View File

@@ -0,0 +1,53 @@
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 root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/render-routing.ts");
const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const routing = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-05/render-routing.json"), "utf8"));
const request = (overrides = {}) => ({ schemaVersion: 1, renderEngine: "BLENDER_EEVEE", backend: "WEBGL2", complexity: "BOUNDED", ...overrides });
test("M11-05 keeps bounded Eevee on the declared local backend", () => {
const result = routing.routeRenderExecution(request());
assert.deepEqual(result, {
schemaVersion: 1, target: "WEB_LOCAL_BOUNDED", status: "READY", capability: "WEB_REALTIME_BOUNDED", reason: "BOUNDED_EEVEE", issues: [],
});
assert.deepEqual({ target: result.target, status: result.status, capability: result.capability, reason: result.reason }, golden.boundedEevee);
assert.equal(routing.routeRenderExecution(request({ backend: "WEBGPU" }), { webgpuAvailable: true, webgpuRendererBundled: true }).status, "READY");
});
test("M11-05 routes Cycles, complex Eevee and hardware to SERVER_JOB without a local success", () => {
for (const [candidate, expected] of [
[request({ renderEngine: "BLENDER_CYCLES", backend: "CYCLES" }), golden.cycles],
[request({ complexity: "COMPLEX", backend: "EEVEE_COMPLEX" }), golden.complexEevee],
[request({ hardwareBackend: "OPTIX" }), golden.hardware],
]) {
const result = routing.routeRenderExecution(candidate);
assert.equal(result.target, expected.target);
assert.equal(result.status, expected.withoutEndpoint.status);
assert.equal(result.reason, expected.withoutEndpoint.reason);
assert.equal(result.issues[0].code, expected.withoutEndpoint.code);
}
const available = routing.routeRenderExecution(request({ renderEngine: "BLENDER_CYCLES", backend: "CYCLES" }), { serverRenderAvailable: true });
assert.deepEqual([available.target, available.status, available.capability], [golden.cycles.target, golden.cycles.withEndpoint.status, golden.cycles.withEndpoint.capability]);
});
test("M11-05 blocks unavailable WebGPU and unknown engines and rejects malformed routes", () => {
const webgpu = routing.routeRenderExecution(request({ backend: "WEBGPU" }));
assert.deepEqual([webgpu.target, webgpu.status, webgpu.issues[0].code], [golden.webgpu.target, golden.webgpu.status, golden.webgpu.code]);
const unknown = routing.routeRenderExecution(request({ renderEngine: "UNKNOWN_ENGINE" }));
assert.deepEqual([unknown.target, unknown.status, unknown.capability, unknown.issues[0].code], [golden.unknownEngine.target, golden.unknownEngine.status, golden.unknownEngine.capability, golden.unknownEngine.code]);
assert.throws(() => routing.routeRenderExecution(request({ backend: "INVALID" })), /INVALID_ARGUMENT/);
assert.throws(() => routing.routeRenderExecution(request({ renderEngine: "invalid engine" })), /INVALID_ARGUMENT/);
});

View File

@@ -0,0 +1,164 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { pathToFileURL } from "node:url";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "sequencer-audio-recovery-unit-"));
const protocolPath = path.join(root, "web/protocol/sequencer-audio-session.ts");
const runtimePath = path.join(root, "web/app/src/sequencer/SequencerAudioSession.ts");
function transpile(source, fileName) {
const result = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName,
reportDiagnostics: true,
});
assert.deepEqual(result.diagnostics, []);
return result.outputText;
}
fs.writeFileSync(
path.join(temporary, "sequencer-audio-session.mjs"),
transpile(fs.readFileSync(protocolPath, "utf8").replace('import type { ErrorCode } from "./error";\n', ""), protocolPath),
);
fs.writeFileSync(
path.join(temporary, "SequencerAudioSession.mjs"),
transpile(
fs.readFileSync(runtimePath, "utf8").replace(
'from "../../../protocol/sequencer-audio-session";',
'from "./sequencer-audio-session.mjs";',
),
runtimePath,
),
);
const protocol = await import(pathToFileURL(path.join(temporary, "sequencer-audio-session.mjs")));
const runtime = await import(pathToFileURL(path.join(temporary, "SequencerAudioSession.mjs")));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-13/sequencer-audio-recovery.json"), "utf8"));
class FakeAudioParam {
value = 1;
events = [];
cancelScheduledValues(time) { this.events.push(["cancel", time]); }
setValueAtTime(value, time) { this.value = value; this.events.push(["set", value, time]); }
}
class FakeGainNode {
gain = new FakeAudioParam();
connected = false;
disconnected = false;
connect() { this.connected = true; }
disconnect() { this.disconnected = true; }
}
class FakeAudioContext {
static instances = [];
state = "suspended";
currentTime = 1;
destination = {};
output = new FakeGainNode();
closeCount = 0;
constructor() { FakeAudioContext.instances.push(this); }
createGain() { return this.output; }
async resume() { this.state = "running"; }
async suspend() { this.state = "suspended"; }
async close() { this.closeCount += 1; this.state = "closed"; }
}
const summary = (report) => [
report.contextState,
report.outputState,
report.muted,
report.outputGain,
report.issueCode,
];
test("M11-13 restores output after real session suspend and mute transitions", async () => {
FakeAudioContext.instances.length = 0;
const session = new runtime.SequencerAudioSession({
scope: { AudioContext: FakeAudioContext },
outputGain: golden.outputGain,
});
const reports = [];
reports.push(await session.initialize());
reports.push(session.setMuted(true));
reports.push(await session.resume());
reports.push(session.setMuted(false));
reports.push(await session.suspend());
reports.push(await session.resume());
reports.push(await session.close());
assert.deepEqual(reports.map(summary), golden.lifecycle);
assert.deepEqual(reports.map((report) => report.revision), [1, 2, 3, 4, 5, 6, 7]);
const context = FakeAudioContext.instances[0];
assert.equal(context.output.gain.value, 0);
assert.equal(context.output.connected, true);
assert.equal(context.output.disconnected, true);
assert.equal(context.closeCount, 1);
assert.ok(context.output.gain.events.some((event) => event[0] === "set" && event[1] === 0));
assert.ok(context.output.gain.events.some((event) => event[0] === "set" && event[1] === golden.outputGain));
});
test("M11-13 blocks a missing audio device and can retry after the runtime becomes available", async () => {
FakeAudioContext.instances.length = 0;
const scope = {};
const session = new runtime.SequencerAudioSession({ scope, outputGain: golden.outputGain });
const missing = await session.initialize();
assert.deepEqual(
[missing.contextState, missing.outputState, missing.issueCode],
golden.missingDevice,
);
assert.deepEqual(session.snapshot(), missing);
scope.AudioContext = FakeAudioContext;
const recovered = await session.recoverDevice();
assert.deepEqual(summary(recovered), golden.lifecycle[0]);
const resumed = await session.resume();
assert.deepEqual(summary(resumed), ["RUNNING", "ENABLED", false, golden.outputGain, null]);
await session.close();
});
test("M11-13 keeps failed resume and malformed reports silent and structured", async () => {
class ResumeFailureContext extends FakeAudioContext {
async resume() { throw new Error("injected resume failure"); }
}
const failed = new runtime.SequencerAudioSession({
contextFactory: () => new ResumeFailureContext(),
outputGain: golden.outputGain,
});
await failed.initialize();
const resumeFailure = await failed.resume();
assert.deepEqual(
[resumeFailure.contextState, resumeFailure.outputState, resumeFailure.issueCode],
golden.resumeFailure,
);
await failed.close();
const unavailable = new runtime.SequencerAudioSession({
contextFactory: () => { throw new Error("no output device"); },
});
assert.deepEqual(
[
(await unavailable.initialize()).contextState,
unavailable.snapshot().outputState,
unavailable.snapshot().issueCode,
],
golden.missingDevice,
);
assert.throws(() => new runtime.SequencerAudioSession({ outputGain: 0 }), {
code: "SEQUENCER_AUDIO_CONTEXT_INVALID",
});
assert.throws(() => protocol.parseSequencerAudioSessionReport({
schemaVersion: 1,
revision: 1,
contextState: "RUNNING",
outputState: "ENABLED",
muted: true,
outputGain: 1,
issueCode: null,
}), { code: "SEQUENCER_AUDIO_CONTEXT_INVALID" });
});

View File

@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
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(), "sequencer-codec-probe-unit-"));
function transpile(sourceName, outputName, replacements = []) {
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, outputName), replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText));
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("asset-path.ts", "asset-path.mjs");
transpile("sequencer.ts", "sequencer.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
['from "./asset-path"', 'from "./asset-path.mjs"'],
]);
const sequencer = await import(pathToFileURL(path.join(temporary, "sequencer.mjs")));
const requests = [
{ schemaVersion: 1, stripType: "IMAGE", mimeType: "image/png", byteLength: 261, sourceSha256: "a".repeat(64) },
{ schemaVersion: 1, stripType: "SOUND", mimeType: "audio/wav", byteLength: 16_044, sourceSha256: "b".repeat(64) },
{ schemaVersion: 1, stripType: "MOVIE", mimeType: "video/mp4", byteLength: 1_484, sourceSha256: "c".repeat(64) },
];
const ready = [
{ backend: "IMAGE_BITMAP", decoded: { width: 8, height: 8 } },
{ backend: "WEB_AUDIO", decoded: { sampleRate: 8_000, channels: 1, durationFrames: 8_000 } },
{ backend: "HTML_MEDIA", decoded: { width: 16, height: 16, durationMicros: 1_000_000 } },
];
test("M11-09 gates IMAGE, SOUND and MOVIE only with identity-bound ready probe receipts", () => {
for (const [index, request] of requests.entries()) {
assert.deepEqual(sequencer.parseSequencerCodecProbeRequest(request), request);
const result = { ...request, status: "READY", backend: ready[index].backend, reason: null, decoded: ready[index].decoded };
assert.equal(sequencer.gateSequencerCodec(request, result).status, "READY");
}
});
test("M11-09 rejects extension fields, family mismatch, forged backend and source drift", () => {
assert.throws(() => sequencer.parseSequencerCodecProbeRequest({ ...requests[0], sourcePath: "image.mp4" }), { code: "SEQUENCER_SCHEMA_INVALID" });
assert.throws(() => sequencer.parseSequencerCodecProbeRequest({ ...requests[0], mimeType: "video/mp4" }), { code: "SEQUENCER_SCHEMA_INVALID" });
const forged = { ...requests[0], status: "READY", backend: "HTML_MEDIA", reason: null, decoded: { width: 8, height: 8 } };
assert.deepEqual(sequencer.gateSequencerCodec(requests[0], forged).issues.map((issue) => issue.code), ["SEQUENCER_CODEC_UNSUPPORTED"]);
const drift = { ...requests[0], sourceSha256: "d".repeat(64), status: "READY", backend: "IMAGE_BITMAP", reason: null, decoded: { width: 8, height: 8 } };
assert.deepEqual(sequencer.gateSequencerCodec(requests[0], drift).issues.map((issue) => issue.code), ["SEQUENCER_CODEC_UNSUPPORTED"]);
});
test("M11-09 keeps runtime unavailability and decode failure blocked", () => {
for (const [index, request] of requests.entries()) {
const result = { ...request, status: "BLOCKED", backend: ready[index].backend, reason: "DECODE_FAILED", decoded: null };
const gate = sequencer.gateSequencerCodec(request, result);
assert.equal(gate.status, "BLOCKED");
assert.deepEqual(gate.issues.map((issue) => issue.code), ["SEQUENCER_CODEC_UNSUPPORTED"]);
}
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,132 @@
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 root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/sequencer-export.ts");
const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const finalExport = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-12/sequencer-final-export.json"), "utf8"));
const request = (overrides = {}) => ({
schemaVersion: 1,
timelineId: "sequencer:scene:SequencerScene",
timelineRevision: 1,
sourceBlendSha256: golden.sourceBlendSha256,
frameStart: 1,
frameEnd: 250,
fpsNumerator: 24000,
fpsDenominator: 1001,
width: 1920,
height: 1080,
container: "MPEG4",
videoCodec: "H264",
audioCodec: "AAC",
...overrides,
});
test("M11-12 requires the declared server export capability", async () => {
const blocked = await finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable: false,
browserVideoEncoderAvailable: false,
});
assert.deepEqual(blocked, golden.withoutServer);
const routed = await finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable: true,
browserVideoEncoderAvailable: false,
});
assert.deepEqual(routed, golden.withServer);
});
test("M11-12 never treats browser VideoEncoder detection as local final-export support", async () => {
for (const serverExportAvailable of [false, true]) {
const withoutEncoder = await finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable,
browserVideoEncoderAvailable: false,
});
const withEncoder = await finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable,
browserVideoEncoderAvailable: true,
});
assert.equal(withoutEncoder.route, "SERVER_EXPORT");
assert.equal(withEncoder.route, "SERVER_EXPORT");
assert.equal(withoutEncoder.localEncoding, "BLOCKED");
assert.equal(withEncoder.localEncoding, "BLOCKED");
assert.equal(withEncoder.browserVideoEncoderDetected, true);
assert.deepEqual(
{ ...withEncoder, browserVideoEncoderDetected: false },
withoutEncoder,
);
}
});
test("M11-12 hashes settings and source revision into deterministic request identities", async () => {
const baseline = await finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable: true,
browserVideoEncoderAvailable: false,
});
assert.equal(baseline.settingsSha256, golden.settingsSha256);
assert.equal(baseline.requestSha256, golden.requestSha256);
assert.deepEqual(
await finalExport.routeSequencerFinalExport({ ...request(), container: "MPEG4" }, {
serverExportAvailable: true,
browserVideoEncoderAvailable: false,
}),
baseline,
);
const revisionDrift = await finalExport.routeSequencerFinalExport(request({ timelineRevision: 2 }), {
serverExportAvailable: true,
browserVideoEncoderAvailable: false,
});
assert.equal(revisionDrift.settingsSha256, baseline.settingsSha256);
assert.notEqual(revisionDrift.requestSha256, baseline.requestSha256);
const settingsDrift = await finalExport.routeSequencerFinalExport(request({ width: 1280 }), {
serverExportAvailable: true,
browserVideoEncoderAvailable: false,
});
assert.notEqual(settingsDrift.settingsSha256, baseline.settingsSha256);
assert.notEqual(settingsDrift.requestSha256, baseline.requestSha256);
});
test("M11-12 rejects undeclared, malformed, and over-budget export requests", async () => {
await assert.rejects(
finalExport.routeSequencerFinalExport({ ...request(), localEncoding: "READY" }, {
serverExportAvailable: true,
browserVideoEncoderAvailable: true,
}),
{ code: "SEQUENCER_EXPORT_REQUEST_INVALID" },
);
await assert.rejects(
finalExport.routeSequencerFinalExport(request({ container: "WEBM", videoCodec: "H264" }), {
serverExportAvailable: true,
browserVideoEncoderAvailable: true,
}),
{ code: "SEQUENCER_EXPORT_REQUEST_INVALID" },
);
await assert.rejects(
finalExport.routeSequencerFinalExport(request({ frameStart: -1_000_000, frameEnd: 1_000_000 }), {
serverExportAvailable: true,
browserVideoEncoderAvailable: true,
}),
{ code: "SEQUENCER_EXPORT_REQUEST_INVALID" },
);
await assert.rejects(
finalExport.routeSequencerFinalExport(request(), {
serverExportAvailable: true,
browserVideoEncoderAvailable: true,
codecName: "h264",
}),
{ code: "SEQUENCER_EXPORT_REQUEST_INVALID" },
);
});

View File

@@ -0,0 +1,120 @@
import assert from "node:assert/strict";
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(), "sequencer-media-cache-unit-"));
function transpile(sourceName, outputName, replacements = []) {
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, outputName), replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText));
}
transpile("capability-gates.ts", "capability-gates.mjs");
transpile("asset-path.ts", "asset-path.mjs");
transpile("sequencer.ts", "sequencer.mjs", [
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
['from "./asset-path"', 'from "./asset-path.mjs"'],
]);
transpile("sequencer-media-cache.ts", "sequencer-media-cache.mjs", [
['from "./sequencer"', 'from "./sequencer.mjs"'],
]);
const mediaCache = await import(pathToFileURL(path.join(temporary, "sequencer-media-cache.mjs")));
const source = {
schemaVersion: 1,
stripType: "MOVIE",
mimeType: "video/mp4",
byteLength: 1_484,
sourceSha256: "a".repeat(64),
};
const capability = {
...source,
status: "READY",
backend: "HTML_MEDIA",
reason: null,
decoded: { width: 16, height: 16, durationMicros: 1_000_000 },
};
const profile = {
kind: "MOVIE_RGBA8_FRAME",
width: 2,
height: 2,
colorSpace: "SRGB8",
alphaMode: "STRAIGHT",
};
const payload = new Uint8Array([
1, 2, 3, 255, 4, 5, 6, 255,
7, 8, 9, 255, 10, 11, 12, 255,
]).buffer;
test("M11-10 binds proxy identity to source, decode receipt, profile and source frame", async () => {
const manifest = await mediaCache.createSequencerMediaCacheManifest(source, capability, profile, 12, payload);
assert.equal(manifest.payloadByteLength, 16);
assert.match(manifest.identitySha256, /^[a-f0-9]{64}$/);
assert.match(manifest.payloadSha256, /^[a-f0-9]{64}$/);
assert.equal(mediaCache.sequencerMediaCacheKey(manifest), `sequencer-media-cache:v1:${manifest.identitySha256}`);
assert.deepEqual(await mediaCache.verifySequencerMediaCacheEntry(manifest, payload, source, capability), manifest);
const reordered = { ...capability, decoded: { durationMicros: 1_000_000, height: 16, width: 16 } };
assert.equal(
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 12),
await mediaCache.computeSequencerMediaCacheIdentity(source, reordered, profile, 12),
);
assert.notEqual(
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 12),
await mediaCache.computeSequencerMediaCacheIdentity(source, capability, profile, 13),
);
});
test("M11-10 rejects source, decode capability, identity and payload drift independently", async () => {
const manifest = await mediaCache.createSequencerMediaCacheManifest(source, capability, profile, 0, payload);
const changedSource = { ...source, sourceSha256: "b".repeat(64) };
const changedSourceCapability = { ...capability, sourceSha256: changedSource.sourceSha256 };
await assert.rejects(
mediaCache.verifySequencerMediaCacheEntry(manifest, payload, changedSource, changedSourceCapability),
{ code: "SEQUENCER_CACHE_SOURCE_MISMATCH" },
);
const changedCapability = { ...capability, decoded: { ...capability.decoded, durationMicros: 999_999 } };
await assert.rejects(
mediaCache.verifySequencerMediaCacheEntry(manifest, payload, source, changedCapability),
{ code: "SEQUENCER_CACHE_CAPABILITY_MISMATCH" },
);
await assert.rejects(
mediaCache.verifySequencerMediaCacheEntry({ ...manifest, identitySha256: "c".repeat(64) }, payload, source, capability),
{ code: "SEQUENCER_CACHE_IDENTITY_MISMATCH" },
);
const corrupted = payload.slice(0);
new Uint8Array(corrupted)[0] ^= 0xff;
await assert.rejects(
mediaCache.verifySequencerMediaCacheEntry(manifest, corrupted, source, capability),
{ code: "SEQUENCER_CACHE_HASH_MISMATCH" },
);
});
test("M11-10 keeps blocked receipts, extension fields and oversized profiles out of cache", async () => {
await assert.rejects(
mediaCache.createSequencerMediaCacheManifest(source, { ...capability, status: "BLOCKED", backend: null, reason: "RUNTIME_UNAVAILABLE", decoded: null }, profile, 0, payload),
{ code: "SEQUENCER_CODEC_UNSUPPORTED" },
);
assert.throws(() => mediaCache.parseSequencerMediaProxyProfile({ ...profile, codec: "h264" }), { code: "SEQUENCER_SCHEMA_INVALID" });
assert.throws(
() => mediaCache.parseSequencerMediaProxyProfile({ ...profile, width: 4096, height: 4097 }),
{ code: "SEQUENCER_BUDGET_EXCEEDED" },
);
await assert.rejects(
mediaCache.createSequencerMediaCacheManifest(source, capability, { ...profile, width: 17 }, 0, new ArrayBuffer(17 * 2 * 4)),
{ code: "SEQUENCER_SCHEMA_INVALID" },
);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
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(), "sequencer-media-revision-unit-"));
const sourcePath = path.join(repoRoot, "web/protocol/sequencer-media-revision.ts");
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, "sequencer-media-revision.mjs"), result.outputText);
const revision = await import(pathToFileURL(path.join(temporary, "sequencer-media-revision.mjs")));
const request = (operation, requestRevision, frame = 24) => ({
schemaVersion: 1,
requestId: `media:${requestRevision}`,
timelineId: "sequencer:main",
timelineRevision: 7,
requestRevision,
operation,
frame,
});
const completed = (value) => ({
...value,
status: "COMPLETED",
sourceFrame: value.frame,
payloadSha256: "a".repeat(64),
});
const state = (latestRequestRevision, timelineRevision = 7) => ({
schemaVersion: 1,
timelineId: "sequencer:main",
timelineRevision,
latestRequestRevision,
});
test("M11-11 publishes matching latest SEEK, SCRUB and DECODE results", () => {
for (const [index, operation] of ["SEEK", "SCRUB", "DECODE"].entries()) {
const active = request(operation, index + 1);
assert.deepEqual(revision.gateSequencerMediaRevision(active, state(index + 1), completed(active)), {
status: "PUBLISH",
code: null,
operation,
requestRevision: index + 1,
});
}
});
test("M11-11 marks superseded and timeline-replaced results stale", () => {
const oldSeek = request("SEEK", 3);
assert.deepEqual(revision.gateSequencerMediaRevision(oldSeek, state(4), completed(oldSeek)), {
status: "STALE", code: "REVISION_CONFLICT", operation: "SEEK", requestRevision: 3,
});
const oldDecode = request("DECODE", 5);
assert.deepEqual(revision.gateSequencerMediaRevision(oldDecode, state(6, 8), completed(oldDecode)), {
status: "STALE", code: "REVISION_CONFLICT", operation: "DECODE", requestRevision: 5,
});
});
test("M11-11 rejects result identity forgery without publishing it", () => {
const active = request("SCRUB", 9, 48);
for (const forged of [
{ ...completed(active), requestId: "media:forged" },
{ ...completed(active), timelineRevision: 8 },
{ ...completed(active), requestRevision: 10 },
{ ...completed(active), operation: "DECODE" },
{ ...completed(active), frame: 49 },
]) {
assert.equal(revision.gateSequencerMediaRevision(active, state(9), forged).status, "STALE");
}
});
test("M11-11 rejects undeclared fields, invalid operations and fractional revisions", () => {
const active = request("SEEK", 1);
assert.throws(() => revision.parseSequencerMediaRevisionRequest({ ...active, signal: "late" }), { code: "SEQUENCER_SCHEMA_INVALID" });
assert.throws(() => revision.parseSequencerMediaRevisionRequest({ ...active, operation: "PLAY" }), { code: "SEQUENCER_SCHEMA_INVALID" });
assert.throws(() => revision.parseSequencerMediaRevisionState({ ...state(1), timelineRevision: 7.5 }), { code: "SEQUENCER_SCHEMA_INVALID" });
assert.throws(() => revision.parseSequencerMediaRevisionResult({ ...completed(active), payloadSha256: "bad" }), { code: "SEQUENCER_SCHEMA_INVALID" });
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,66 @@
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 root = path.resolve(import.meta.dirname, "../../..");
const sourcePath = path.join(root, "web/protocol/server-render-job.ts");
const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const jobs = await import("data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"));
const sourceBytes = new TextEncoder().encode("server-render-source-v1").buffer;
const build = { version: "5.2.0", buildSha256: "a".repeat(64) };
const settings = {
renderEngine: "BLENDER_CYCLES",
frameStart: 1,
frameEnd: 1,
resolutionX: 32,
resolutionY: 32,
resolutionPercentage: 100,
samples: 4,
outputMime: "image/png",
transparent: false,
};
test("M11-06 binds source bytes, Blender build and canonical render settings", async () => {
const request = await jobs.createServerRenderJobRequest(sourceBytes, build, settings, { jobId: "render:test", sourceRevision: 7 });
assert.equal(request.sourceBlendByteLength, sourceBytes.byteLength);
assert.match(request.sourceBlendSha256, /^[a-f0-9]{64}$/);
assert.match(request.settingsSha256, /^[a-f0-9]{64}$/);
assert.match(request.requestSha256, /^[a-f0-9]{64}$/);
assert.deepEqual(await jobs.verifyServerRenderJobRequest(request, sourceBytes), request);
const reordered = await jobs.createServerRenderJobRequest(sourceBytes, build, { transparent: settings.transparent, ...settings }, { jobId: "render:test", sourceRevision: 7 });
assert.equal(reordered.settingsSha256, request.settingsSha256);
assert.equal(await jobs.createServerRenderJobKey(request), await jobs.createServerRenderJobKey(reordered));
});
test("M11-06 verifies output hash and rejects every provenance drift", async () => {
const request = await jobs.createServerRenderJobRequest(sourceBytes, build, settings, { jobId: "render:verify", sourceRevision: 2 });
const output = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01]).buffer;
const result = await jobs.createServerRenderJobResult(request, output);
assert.equal(result.status, "SUCCEEDED");
assert.deepEqual(await jobs.verifyServerRenderJobResult(result, request, output), result);
await assert.rejects(jobs.verifyServerRenderJobRequest(request, Uint8Array.from([1, 2, 3]).buffer), { code: "SERVER_RENDER_SOURCE_HASH_MISMATCH" });
await assert.rejects(jobs.verifyServerRenderJobResult(result, request, Uint8Array.from([9, 9]).buffer), { code: "SERVER_RENDER_OUTPUT_HASH_MISMATCH" });
await assert.rejects(jobs.verifyServerRenderJobResult({ ...result, sourceBlendSha256: "b".repeat(64) }, request, output), { code: "SERVER_RENDER_BINDING_MISMATCH" });
await assert.rejects(jobs.verifyServerRenderJobRequest({ ...request, blenderBuild: { ...build, buildSha256: "b".repeat(64) } }), { code: "SERVER_RENDER_REQUEST_HASH_MISMATCH" });
await assert.rejects(jobs.createServerRenderJobRequest(sourceBytes, { ...build, version: "5.3.0" }, settings), { code: "SERVER_RENDER_BUILD_INVALID" });
await assert.rejects(jobs.createServerRenderJobRequest(sourceBytes, build, { ...settings, samples: 0 }), { code: "SERVER_RENDER_SETTINGS_INVALID" });
});
test("M11-06 rejects malformed result metadata and settings budget abuse", async () => {
await assert.rejects(jobs.createServerRenderJobRequest(sourceBytes, build, { ...settings, payload: "x".repeat(300_000) }), { code: "SERVER_RENDER_SETTINGS_INVALID" });
const request = await jobs.createServerRenderJobRequest(sourceBytes, build, settings);
const result = await jobs.createServerRenderJobResult(request, Uint8Array.from([1]).buffer);
await assert.rejects(jobs.verifyServerRenderJobResult({ ...result, outputByteLength: result.outputByteLength + 1 }, request), { code: "SERVER_RENDER_RESULT_HASH_MISMATCH" });
await assert.rejects(jobs.verifyServerRenderJobResult({ ...result, outputMime: "image/openexr" }, request), { code: "SERVER_RENDER_OUTPUT_INVALID" });
await assert.rejects(jobs.verifyServerRenderJobResult({ ...result, errorCode: "SERVER_RENDER_FAILED" }, request), { code: "SERVER_RENDER_OUTPUT_INVALID" });
await assert.rejects(jobs.verifyServerRenderJobRequest({ ...request, unexpected: true }), { code: "SERVER_RENDER_REQUEST_INVALID" });
});

View File

@@ -0,0 +1,179 @@
import assert from "node:assert/strict";
import { createHash } 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(), "shader-compiler-unit-"));
const sourcePath = path.join(root, "web/protocol/shader-compiler.ts");
const outputPath = path.join(temporary, "shader-compiler.mjs");
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(outputPath, transpiled.outputText);
const compiler = await import(pathToFileURL(outputPath));
function material(overrides = {}) {
return {
id: "material:ShaderUnit",
name: "ShaderUnit",
baseColor: [0.2, 0.3, 0.4, 1],
roughness: 0.5,
metallic: 0.1,
emissionColor: [0, 0, 0, 1],
alpha: 1,
ior: 1.45,
shaderGraphHash: "a".repeat(64),
nodes: [
{ id: "rgb", type: "RGB", name: "RGB", defaultValue: [0.1, 0.2, 0.3, 1] },
{ id: "value-a", type: "VALUE", name: "A", defaultValue: [0.2] },
{ id: "value-b", type: "VALUE", name: "B", defaultValue: [0.22] },
{ id: "math", type: "MATH", name: "Add", properties: { operation: "ADD" } },
{ id: "image", type: "IMAGE_TEXTURE", name: "Image", imageId: "image:Normal" },
{ id: "normal", type: "NORMAL_MAP", name: "Normal" },
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
{ id: "output", type: "OUTPUT", name: "Output" },
],
links: [
{ fromNodeId: "rgb", fromSocket: "Color", toNodeId: "principled", toSocket: "Base Color" },
{ fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" },
{ fromNodeId: "value-b", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" },
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" },
{ fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" },
{ fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" },
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
],
...overrides,
};
}
test("M10-07 compiles the declared Principled/Image/Normal/Math closure", () => {
const report = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]) });
assert.equal(report.status, "COMPILED");
assert.equal(report.taskId, "M10-07");
assert.equal(report.backend, "WEBGL2_THREE_PHYSICAL");
assert.equal(report.material.baseColor[2], 0.3);
assert.ok(Math.abs(report.material.roughness - 0.42) < 1e-8);
assert.deepEqual(report.textureBindings, [{ imageId: "image:Normal", usage: "NORMAL" }]);
assert.match(report.graphHash, /^[0-9a-f]{64}$/);
assert.ok(report.instructions.some((instruction) => instruction.operation === "ADD"));
assert.ok(report.nodeOrder.indexOf("value-a") < report.nodeOrder.indexOf("math"));
assert.ok(report.nodeOrder.indexOf("math") < report.nodeOrder.indexOf("principled"));
assert.ok(report.nodeOrder.indexOf("principled") < report.nodeOrder.indexOf("output"));
});
test("M10-07 blocks unknown nodes and preserves the graph metadata", () => {
const source = material();
source.nodes.push({ id: "mix", type: "UNSUPPORTED", name: "ShaderNodeMix" });
const report = compiler.compileMaterialGraph(source);
assert.equal(report.status, "BLOCKED");
assert.equal(report.issues[0].code, "SHADER_NODE_UNSUPPORTED");
assert.equal(source.nodes.at(-1).type, "UNSUPPORTED");
});
test("M10-07 rejects cycles, duplicate links, and missing image resources", () => {
const cyclic = material({
links: [
{ fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" },
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" },
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" },
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
{ fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" },
{ fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" },
],
});
const cyclicReport = compiler.compileMaterialGraph(cyclic);
assert.equal(cyclicReport.status, "BLOCKED");
assert.ok(cyclicReport.issues.some((issue) => issue.code === "SHADER_GRAPH_CYCLE"));
const missingReport = compiler.compileMaterialGraph(material(), { imageIds: new Set() });
assert.equal(missingReport.status, "BLOCKED");
assert.ok(missingReport.issues.some((issue) => issue.code === "SHADER_EXTERNAL_RESOURCE_MISSING"));
});
test("M10-07 keeps the fallback graph fingerprint deterministic", () => {
const source = material();
delete source.shaderGraphHash;
const first = compiler.compileMaterialGraph(source);
const second = compiler.compileMaterialGraph(source);
assert.equal(first.graphHash, second.graphHash);
assert.match(first.graphHash, /^[0-9a-f]{64}$/);
const canonical = JSON.stringify({
schemaVersion: 1,
materialId: source.id,
nodes: source.nodes.map((node) => ({
id: node.id,
type: node.type,
name: node.name,
imageId: node.imageId ?? null,
defaultValue: node.defaultValue ?? null,
properties: node.properties ?? null,
})),
links: source.links.map((link) => ({
fromNodeId: link.fromNodeId,
fromSocket: link.fromSocket,
toNodeId: link.toNodeId,
toSocket: link.toSocket,
})),
});
assert.equal(first.graphHash, createHash("sha256").update(canonical).digest("hex"));
});
test("M10-07 fails closed before oversized topology or forged graph hashes are compiled", () => {
const oversized = material({
nodes: Array.from({ length: compiler.SHADER_COMPILE_BUDGET.maxNodes + 1 }, (_value, index) => ({
id: `value:${index}`,
type: "VALUE",
name: `Value ${index}`,
defaultValue: [0],
})),
links: [],
});
const oversizedReport = compiler.compileMaterialGraph(oversized);
assert.equal(oversizedReport.status, "BLOCKED");
assert.equal(oversizedReport.issues[0].code, "SHADER_NODE_UNSUPPORTED");
const forged = compiler.compileMaterialGraph(material({ shaderGraphHash: "A".repeat(64) }));
assert.equal(forged.status, "BLOCKED");
assert.equal(forged.issues[0].code, "SHADER_INVALID_GRAPH");
});
test("M10-08 binds graph, texture identity, color space and backend into compileKey", () => {
const textureIdentities = new Map([["image:Normal", {
assetId: "asset:normal-v1",
sha256: "b".repeat(64),
colorSpace: "NON_COLOR",
}]]);
const first = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities });
const second = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), sha256: "c".repeat(64) }]]) });
const linear = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), colorSpace: "LINEAR" }]]) });
assert.equal(first.status, "COMPILED");
assert.match(first.compileKey, /^[0-9a-f]{64}$/);
assert.notEqual(first.compileKey, second.compileKey);
assert.notEqual(first.compileKey, linear.compileKey);
assert.notEqual(
first.compileKey,
compiler.createShaderCompileKey({
graphHash: first.graphHash,
rendererBackend: "WEBGPU",
textures: [{ imageId: "image:Normal", usage: "NORMAL", assetId: "asset:normal-v1", sha256: "b".repeat(64), colorSpace: "NON_COLOR" }],
}),
);
});
test("M10-08 rejects an unknown renderer backend and malformed texture identity", () => {
const backend = compiler.compileMaterialGraph(material(), { rendererBackend: "WEBGPU" });
assert.equal(backend.status, "BLOCKED");
assert.equal(backend.issues[0].code, "CAPABILITY_MISSING");
const malformed = compiler.compileMaterialGraph(material(), {
imageIds: new Set(["image:Normal"]),
textureIdentities: new Map([["image:Normal", { sha256: "not-a-digest" }]]),
});
assert.equal(malformed.status, "BLOCKED");
assert.equal(malformed.issues.at(-1).code, "SHADER_INVALID_GRAPH");
});

View File

@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
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(), "simulation-cache-unit-"));
const sourcePath = path.join(root, "web/protocol/simulation-cache.ts");
const outputPath = path.join(temporary, "simulation-cache.mjs");
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(outputPath, transpiled.outputText);
const simulation = await import(pathToFileURL(outputPath));
const digest = async (value) => Array.from(
new Uint8Array(await crypto.subtle.digest("SHA-256", value)),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
async function manifest(overrides = {}) {
const payload = Uint8Array.from([1, 2, 3, 4]).buffer;
const binding = {
graphId: "node-group:SimulationUnit",
graphHash: await digest(Uint8Array.from([1]).buffer),
sourceBlendSha256: await digest(Uint8Array.from([2]).buffer),
sourceRevision: 7,
inputHash: await digest(Uint8Array.from([3]).buffer),
blenderVersion: "5.2.0",
frameStart: 1,
frameEnd: 1,
};
return {
schemaVersion: 2,
...binding,
revisionHash: await simulation.computeSimulationCacheRevisionHash(binding),
cacheSha256: await digest(payload),
byteLength: payload.byteLength,
frames: [{ frame: 1, byteOffset: 0, byteLength: payload.byteLength, sha256: await digest(payload) }],
...overrides,
};
}
test("M10-05 binds a cache key to the full graph, source, revision and input identity", async () => {
const value = await manifest();
const verified = await simulation.verifySimulationCacheRevisionBinding(value);
assert.equal(verified.sourceRevision, 7);
assert.equal(simulation.simulationCacheKey(verified), `sim2-${value.revisionHash}`);
assert.equal(simulation.simulationCacheKey(verified).length, 69);
});
test("M10-05 rejects graph, source, revision, input and range drift", async () => {
const value = await manifest();
for (const patch of [
{ graphHash: "0".repeat(64) },
{ sourceBlendSha256: "1".repeat(64) },
{ sourceRevision: 8 },
{ inputHash: "2".repeat(64) },
{ frameStart: 2, frameEnd: 2, frames: [{ ...value.frames[0], frame: 2 }] },
]) {
await assert.rejects(
simulation.verifySimulationCacheRevisionBinding({ ...value, ...patch }),
{ code: "SIMULATION_CACHE_REVISION_MISMATCH" },
);
}
});
test("M10-05 rejects legacy schemas, undeclared fields and invalid revisions", async () => {
const value = await manifest();
assert.throws(() => simulation.parseSimulationCacheManifest({ ...value, schemaVersion: 1 }), { code: "PROTOCOL_MISMATCH" });
assert.throws(() => simulation.parseSimulationCacheManifest({ ...value, sourceRevision: -1 }), { code: "SIMULATION_CACHE_INVALID" });
assert.throws(() => simulation.parseSimulationCacheManifest({ ...value, staleKey: "accepted" }), { code: "SIMULATION_CACHE_INVALID" });
assert.throws(() => simulation.parseSimulationCacheManifest({
...value,
frames: [{ ...value.frames[0], payload: [1, 2, 3, 4] }],
}), { code: "SIMULATION_CACHE_INVALID" });
});
test("M10-15 reports cache byte and frame budgets before allocating payload bytes", async () => {
const value = await manifest();
assert.throws(() => simulation.parseSimulationCacheManifest({
...value,
byteLength: simulation.SIMULATION_CACHE_BUDGET.maxCacheBytes + 1,
}), { code: "SIMULATION_CACHE_BUDGET_EXCEEDED" });
assert.throws(() => simulation.parseSimulationCacheManifest({
...value,
frameEnd: value.frameStart + simulation.SIMULATION_CACHE_BUDGET.maxFrames,
}), { code: "SIMULATION_CACHE_BUDGET_EXCEEDED" });
});
test("M10-06 plans deterministic LRU eviction while retaining protected playback caches", () => {
const key = (digit) => `sim2-${digit.repeat(64)}`;
const candidates = [
{ cacheKey: key("a"), byteLength: 4, createdAt: "2026-08-16T12:00:00.000Z", lastAccessAt: "2026-08-16T12:00:00.000Z" },
{ cacheKey: key("b"), byteLength: 4, createdAt: "2026-08-16T12:00:01.000Z", lastAccessAt: "2026-08-16T12:00:01.000Z" },
{ cacheKey: key("c"), byteLength: 4, createdAt: "2026-08-16T12:00:02.000Z", lastAccessAt: "2026-08-16T12:00:02.000Z" },
];
const plan = simulation.planSimulationCacheLRU(candidates, 4, [key("a")]);
assert.deepEqual(plan.cacheKeys, [key("b"), key("c")]);
assert.deepEqual(plan.protectedCacheKeys, [key("a")]);
assert.equal(plan.beforeBytes, 12);
assert.equal(plan.remainingBytes, 4);
assert.equal(plan.removedBytes, 8);
assert.equal(plan.budgetSatisfied, true);
});
test("M10-06 reports an unsatisfied LRU budget instead of evicting an active cache", () => {
const cacheKey = `sim2-${"d".repeat(64)}`;
const plan = simulation.planSimulationCacheLRU([
{ cacheKey, byteLength: 8, createdAt: "2026-08-16T12:00:00.000Z", lastAccessAt: "2026-08-16T12:00:00.000Z" },
], 0, [cacheKey]);
assert.deepEqual(plan.cacheKeys, []);
assert.equal(plan.remainingBytes, 8);
assert.equal(plan.budgetSatisfied, false);
});
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));

View File

@@ -0,0 +1,29 @@
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/storage-budget.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 { createStorageBudget, formatStorageBytes } = await import(moduleUrl);
test("M7-11 storage budget reports stable category totals", () => {
const budget = createStorageBudget("budget", { projectBytes: 10, snapshotBytes: 20, lodBytes: 30, mediaBytes: 40, vdbBytes: 50 });
assert.deepEqual(budget, { schemaVersion: 1, projectId: "budget", projectBytes: 10, snapshotBytes: 20, lodBytes: 30, mediaBytes: 40, vdbBytes: 50, totalBytes: 150 });
assert.equal(formatStorageBytes(1024), "1.0 KiB");
assert.equal(formatStorageBytes(1024 * 1024), "1.0 MiB");
});
test("M7-11 storage budget rejects invalid or overflowing categories", () => {
assert.throws(() => createStorageBudget("budget", { mediaBytes: -1 }), /STORAGE_BUDGET_INVALID/);
assert.throws(() => createStorageBudget("budget", { projectBytes: Number.MAX_SAFE_INTEGER, mediaBytes: Number.MAX_SAFE_INTEGER }), /STORAGE_BUDGET_INVALID/);
assert.throws(() => formatStorageBytes(-1), /STORAGE_BUDGET_INVALID/);
});

View File

@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
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(), "texture-paint-asset-unit-"));
function transpile(sourceName, outputName, replacements = []) {
const sourcePath = path.join(repoRoot, "web/protocol", sourceName);
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 output = replacements.reduce((source, [from, to]) => source.replace(from, to), transpiled.outputText);
fs.writeFileSync(path.join(temporary, outputName), output);
}
transpile("paint.ts", "paint.mjs");
transpile("texture-paint-asset.ts", "texture-paint-asset.mjs", [['from "./paint"', 'from "./paint.mjs"']]);
const protocol = await import(pathToFileURL(path.join(temporary, "texture-paint-asset.mjs")));
const hash = "1".repeat(64);
const target = {
schemaVersion: 1,
projectId: "texture-project",
imageId: "image:Paint",
textureAssetId: "image:Paint:tile:1001",
kind: "PACKED",
tile: 1001,
revision: 4,
width: 2,
height: 2,
mimeType: "image/png",
colorSpace: "SRGB",
sourcePath: "textures/paint.png",
baseAssetSha256: hash,
};
const patch = {
schemaVersion: 1,
textureAssetId: target.textureAssetId,
tile: 1001,
revision: 4,
width: 2,
height: 2,
format: "RGBA8",
colorSpace: "SRGB",
baseSha256: "2".repeat(64),
resultSha256: "3".repeat(64),
byteOffset: 0,
bytes: new Uint8Array([1, 2, 3, 255]),
};
test("M9-11 binds a dirty range to one packed or UDIM tile identity", () => {
const parsed = protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target, patch });
assert.equal(parsed.target.kind, "PACKED");
assert.equal(parsed.patch.textureAssetId, target.textureAssetId);
assert.equal(protocol.texturePaintTileBindingKey({ schemaVersion: 1, projectId: target.projectId, textureAssetId: target.textureAssetId, tile: 1001 }), "texture-paint:v1:texture-project:image%3APaint%3Atile%3A1001:1001");
});
test("M9-11 rejects target, tile, revision and path drift before storage", () => {
assert.throws(() => protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target, patch: { ...patch, tile: 1002 } }), /PAINT_SCHEMA_INVALID/);
assert.throws(() => protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target, patch: { ...patch, revision: 5 } }), /PAINT_SCHEMA_INVALID/);
assert.throws(() => protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target: { ...target, sourcePath: "../escape.png" }, patch }), /PAINT_SCHEMA_INVALID/);
assert.throws(() => protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target: { ...target, baseAssetSha256: "bad" }, patch }), /PAINT_SCHEMA_INVALID/);
assert.throws(() => protocol.validateTexturePaintTileCommit({ schemaVersion: 1, target, patch, extra: true }), /PAINT_SCHEMA_INVALID/);
});

View File

@@ -0,0 +1,35 @@
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/ui-schema.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 { createDefaultWebWorkspaceState, reduceUICommand } = await import(moduleUrl);
test("M7-13 menu and modal overlays are mutually exclusive and Escape-closeable", () => {
let state = createDefaultWebWorkspaceState();
state = reduceUICommand(state, { type: "toggleMenu", menu: "文件" });
assert.equal(state.openMenu, "文件");
assert.equal(state.operatorSearchOpen, false);
state = reduceUICommand(state, { type: "toggleMenu", menu: "编辑" });
assert.equal(state.openMenu, "编辑");
assert.equal(state.operatorSearchOpen, false);
state = reduceUICommand(state, { type: "toggleOperatorSearch", open: true });
assert.equal(state.operatorSearchOpen, true);
assert.equal(state.openMenu, null);
state = reduceUICommand(state, { type: "toggleOperatorSearch", open: false });
assert.equal(state.operatorSearchOpen, false);
state = reduceUICommand(state, { type: "toggleMenu", menu: "窗口" });
assert.equal(state.openMenu, "窗口");
state = reduceUICommand(state, { type: "toggleMenu", menu: "窗口" });
assert.equal(state.openMenu, null);
});

View File

@@ -0,0 +1,31 @@
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/viewport-camera.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 { VIEWPORT_DEFAULT_ORBIT, applyOrbitDelta, cameraState, orbitPosition, orbitStateFromPosition } = await import(moduleUrl);
test("M7-15 main and Offscreen orbit camera contract is deterministic", () => {
const position = orbitPosition(VIEWPORT_DEFAULT_ORBIT);
assert.deepEqual(position.map((value) => Number(value.toFixed(6))), [4.219781, -4.219781, 3.658811]);
const restored = orbitStateFromPosition(position);
assert.ok(Math.abs(restored.yaw - VIEWPORT_DEFAULT_ORBIT.yaw) < 1e-12);
assert.ok(Math.abs(restored.pitch - VIEWPORT_DEFAULT_ORBIT.pitch) < 1e-12);
assert.ok(Math.abs(restored.distance - VIEWPORT_DEFAULT_ORBIT.distance) < 1e-12);
assert.deepEqual(restored.target, [0, 0, 0]);
const next = applyOrbitDelta({ ...VIEWPORT_DEFAULT_ORBIT, target: [0, 0, 0] }, 24, -12, 120);
assert.equal(next.yaw, VIEWPORT_DEFAULT_ORBIT.yaw - 24 * 0.008);
assert.equal(next.pitch, VIEWPORT_DEFAULT_ORBIT.pitch - 12 * 0.008);
assert.equal(next.distance, VIEWPORT_DEFAULT_ORBIT.distance * Math.exp(0.12));
assert.deepEqual(cameraState(next).position, orbitPosition(next));
});

View File

@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
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(), "weight-paint-unit-"));
const sourcePath = path.join(repoRoot, "web/protocol/weight-paint.ts");
const outputPath = path.join(temporary, "weight-paint.mjs");
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(outputPath, transpiled.outputText);
const weightPaint = await import(pathToFileURL(outputPath));
const vertices = [
{ index: 0, position: [-1, 0, 0], influences: [{ group: "Root", weight: 1 }, { group: "Tip", weight: 0.25 }] },
{ index: 1, position: [1, 0, 0], influences: [{ group: "Root", weight: 1 }, { group: "Tip", weight: 0.25 }] },
{ index: 2, position: [0, 1, 0], influences: [{ group: "Root", weight: 0.25 }, { group: "Tip", weight: 1 }] },
];
test("M9-12 normalizes after limiting influences", () => {
const result = weightPaint.applyWeightPaintPatch(vertices, {
schemaVersion: 1,
vertexGroup: "WebPaintGroup",
indices: [2],
values: [0.5],
normalize: true,
limit: 2,
});
assert.deepEqual(result[2].influences, [
{ group: "Tip", weight: 2 / 3 },
{ group: "WebPaintGroup", weight: 1 / 3 },
]);
});
test("M9-12 mirrors onto a reciprocal verified coordinate map", () => {
const result = weightPaint.applyWeightPaintPatch(vertices, {
schemaVersion: 1,
vertexGroup: "WebPaintGroup",
indices: [0],
values: [0.8],
mirror: true,
mirrorAxis: 0,
mirrorTolerance: 1e-4,
});
assert.equal(result[0].influences.at(-1).weight, 0.8);
assert.equal(result[1].influences.at(-1).weight, 0.8);
});
test("M9-12 rejects an unverified symmetry map and invalid limits", () => {
assert.throws(() => weightPaint.parseWeightPaintOptions({ limit: 33 }), /PAINT_SCHEMA_INVALID/);
assert.throws(() => weightPaint.applyWeightPaintPatch([
...vertices,
{ index: 3, position: [4, 0, 0], influences: [] },
], {
schemaVersion: 1,
vertexGroup: "WebPaintGroup",
indices: [0],
values: [0.8],
mirror: true,
mirrorAxis: 0,
mirrorTolerance: 1e-4,
}), /WEIGHT_MIRROR_SYMMETRY_UNVERIFIED/);
});

View File

@@ -0,0 +1,39 @@
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/worker-fault.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 { createWorkerFault } = await import(moduleUrl);
test("M7-08 maps engine and storage crashes to one recoverable fault contract", () => {
assert.deepEqual(createWorkerFault("engine", "engine crashed"), {
source: "engine",
error: {
code: "WORKER_TERMINATED",
severity: "error",
message: "engine crashed",
recoverable: true,
cause: "engine-worker-fault",
},
});
assert.deepEqual(createWorkerFault("storage", "storage crashed"), {
source: "storage",
error: {
code: "WORKER_TERMINATED",
severity: "error",
message: "storage crashed",
recoverable: true,
cause: "storage-worker-fault",
},
});
});