122 lines
5.0 KiB
JavaScript
122 lines
5.0 KiB
JavaScript
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 }));
|