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")); });