Advance M8-M11 parity workflows
This commit is contained in:
123
web/tests/unit/simulation-cache.test.mjs
Normal file
123
web/tests/unit/simulation-cache.test.mjs
Normal 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 }));
|
||||
Reference in New Issue
Block a user