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