92 lines
4.0 KiB
JavaScript
92 lines
4.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(), "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");
|
|
});
|