Files
hooks-lib/tests/verify-hooks-lib.mjs
Kaysser Kayyali d038eb8c67 v0.3.0: rename module id hax-hooks-lib -> foundry-hooks-lib
User callout: 'Hax' is Kaysser's nickname. The module id should
not use it. Rename the Foundry module id from 'hax-hooks-lib' to
'foundry-hooks-lib'. Gitea repo name stays as 'hooks-lib' (kept
for the user-facing URL); the Gitea manifest URL is unchanged.

**Scope of rename:**
- module.json: id, title, version (0.2.0 -> 0.3.0), download URL
- package.json: name
- README.md, HOOK_CONTRACT.md, LICENSE: branding text
- All 6 production JS files: MODULE_ID constant + comments
- 4 active test files: console.log strings + test descriptions
- Rename of release zips in git: hooks-lib-X.Y.Z.zip ->
  foundry-hooks-lib-X.Y.Z.zip (preserves the v0.1.0 and v0.2.0
  zips as historical artifacts; the v0.3.0 zip is the new
  release artifact)
- .gitignore: glob + un-ignore lines updated to match

**Out of scope (deliberate):**
- Gitea repo name 'kaykayyali/hooks-lib' stays. Per the user's
  direction, only the module id is renamed; the Gitea URL path
  is preserved for the existing 'url', 'manifest', 'download'
  fields.
- scripts/_archive/v0.1.0/*: historical v0.1.0 code is left
  as-is. Those files tested 'hax-hooks-lib v0.1.0'; rewriting
  the history would be misleading.
- tests/_archive_v0.1.0_*.mjs: same reason, left untouched.
- .hermes/plans/* session-historian plans that reference
  'Hax's Tools split': session artifact, not a release asset.

**Verification:** 554/554 smoke assertions pass, 6/6 perf
assertions pass, median 0.0004ms/fire (well under 0.1ms
budget). No logic change; rename is string-only.

**Consumer action required:** battle-focus and its-achievable
both declare 'relationships.requires' pointing to
'hax-hooks-lib'. The next commits on those repos will update
their relationships to 'foundry-hooks-lib' + bump their
versions. Foundry instances with v0.2.0 of the old id
installed will need to be reinstalled as v0.3.0 of the new
id.
2026-06-20 16:53:37 -04:00

539 lines
22 KiB
JavaScript

// tests/verify-hooks-lib.mjs — v0.3.0
//
// Smoke test for the generic Foundry hook facade. Implements
// tests/PLAN.md sections A-G (envelope shape, subscriber API, error
// containment, lifecycle, anti-corruption, adapter loading, perf).
// Runs in <2s without a live Foundry.
import {
installStubs,
resetStubs,
getListeners,
getAllCallLog,
clearCallLog,
setGameVersion,
setGameSystem,
} from "./test-helpers.mjs";
// We import the library's internal modules directly so we can drive
// them without going through the Foundry init/ready lifecycle. The
// public main.js (which calls install() in init) is tested via
// section D's lifecycle tests below.
import { HOOK_REGISTRY, REGISTERED_HOOKS, SYNTHESIZED_ENVELOPES } from "../scripts/internal/registered-hooks.js";
import { buildEnvelope } from "../scripts/internal/envelope.js";
import {
subscribe,
subscribeMany,
subscribeAll,
unsubscribeAll,
listSubscribedHooks,
} from "../scripts/internal/subscribers.js";
import {
registerSystemAdapter,
evaluateAtReady,
listActiveAdapters,
listFailedAdapters,
reset as resetAdapters,
} from "../scripts/internal/adapter-registry.js";
import { install, uninstall, evaluateAdaptersAtReady, listInstalledRawHooks } from "../scripts/internal/lifecycle.js";
import { matchRange } from "../scripts/internal/semver.js";
// Import main.js for its side-effect of registering Foundry hooks in
// Foundry's init. In the smoke test we don't run Foundry's init, but
// importing the file ensures the module's top-level syntax is valid.
// The actual lifecycle is driven via lifecycle.js in the tests below.
// (main.js's Hooks.once("init", ...) and Hooks.once("ready", ...)
// only fire when Foundry itself drives the init/ready cycle.)
const ASSERTIONS = [];
function assert(name, cond, extra = "") {
ASSERTIONS.push({ name, pass: !!cond, extra });
if (cond) {
console.log(`${name}`);
} else {
console.log(`${name} ${extra}`);
}
}
function assertEq(name, actual, expected) {
const ok = JSON.stringify(actual) === JSON.stringify(expected);
ASSERTIONS.push({ name, pass: ok, extra: ok ? "" : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` });
if (ok) console.log(`${name}`);
else console.log(`${name} expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
async function mainTest() {
console.log("--- foundry-hooks-lib v0.3.0 smoke test ---");
// ----- Section F.1 — semver range matcher (foundation for G) -----
console.log("[F.1] semver range matcher");
assert("matchRange exact", matchRange("1.2.3", "1.2.3"));
assert("matchRange >= with equal", matchRange("5.2.5", ">=5.2.0"));
assert("matchRange >= with greater", matchRange("6.0.0", ">=5.2.0"));
assert("matchRange >= with lesser", !matchRange("5.1.9", ">=5.2.0"));
assert("matchRange < with lesser", matchRange("5.2.99", "<5.3.0"));
assert("matchRange < with equal", !matchRange("5.3.0", "<5.3.0"));
assert("matchRange AND range", matchRange("5.2.5", ">=5.2.0 <5.3.0"));
assert("matchRange AND range misses lower", !matchRange("5.1.0", ">=5.2.0 <5.3.0"));
assert("matchRange AND range misses upper", !matchRange("5.3.0", ">=5.2.0 <5.3.0"));
assert("matchRange * matches anything", matchRange("0.0.0", "*"));
assert("matchRange undefined matches anything", matchRange("1.0.0", undefined));
assert("matchRange strips leading v", matchRange("v5.2.5", ">=5.2.0"));
assert("matchRange throws on bad version", (() => { try { matchRange("not-a-version", ">=1.0.0"); return false; } catch { return true; } })());
assert("matchRange throws on bad range op", (() => { try { matchRange("1.0.0", ">>1.0.0"); return false; } catch { return true; } })());
// ----- Section A — Envelope shape -----
installStubs();
console.log("[A] Envelope shape");
for (const entry of HOOK_REGISTRY) {
if (entry.synthesized) continue; // synthesized envelopes don't have a primary build path
for (const rawName of entry.raw) {
// Build a synthetic fire.
const args = syntheticArgsFor(rawName);
const result = buildEnvelope(rawName, args);
assert(`A.${entry.envelope}.${rawName}: buildEnvelope returns result`, result !== null);
if (!result) continue;
assert(`A.${entry.envelope}.${rawName}: envelope is an object`, typeof result.envelope === "object" && result.envelope !== null);
const env = result.envelope;
assert(`A.${entry.envelope}.${rawName}: envelope.ts is number >= 0`, typeof env.ts === "number" && env.ts >= 0);
assert(`A.${entry.envelope}.${rawName}: envelope.hook is string`, typeof env.hook === "string");
assertEq(`A.${entry.envelope}.${rawName}: envelope.hook is "${entry.envelope}"`, env.hook, entry.envelope);
assert(`A.${entry.envelope}.${rawName}: envelope.args is array`, Array.isArray(env.args));
// No extra fields.
const keys = Object.keys(env).sort();
assertEq(`A.${entry.envelope}.${rawName}: envelope has exactly {ts, hook, args}`, keys, ["args", "hook", "ts"]);
}
}
// ----- Section B — Subscriber API -----
resetStubs();
installStubs();
unsubscribeAll();
console.log("[B] Subscriber API");
// B.1 subscribe(hookName, fn) basic.
uninstall();
install();
unsubscribeAll();
clearCallLog();
let received = null;
const u1 = subscribe("updateActor", (env) => { received = env; });
Hooks.callAll("updateActor", { id: "a1" }, { name: "Bob" }, {}, "u1");
// Async dispatch — wait for microtask.
await new Promise((r) => setTimeout(r, 10));
assert("B.1: subscribe receives envelope on async dispatch", received && received.hook === "updateActor");
u1();
// B.2 unsubscribe removes.
received = null;
const u2 = subscribe("updateActor", () => { received = "should-not-fire"; });
u2();
Hooks.callAll("updateActor", { id: "a2" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assert("B.2: unsubscribed callback does NOT fire", received === null);
// B.3 stacking: order preserved.
const seen = [];
const u3a = subscribe("createCombatant", () => seen.push("a"));
const u3b = subscribe("createCombatant", () => seen.push("b"));
Hooks.callAll("createCombatant", { id: "c1" }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.3: multiple subscribers fire in registration order", seen, ["a", "b"]);
u3a();
u3b();
// B.4 subscribe with unregistered hook throws.
let threw = null;
try { subscribe("not-a-real-hook", () => {}); } catch (e) { threw = e; }
assert("B.4: subscribe with unregistered hook throws TypeError", threw instanceof TypeError);
// B.5 subscribeMany atomic.
unsubscribeAll();
const seen5 = [];
let threw5 = null;
try {
subscribeMany({
updateActor: (env) => seen5.push(env.hook),
"not-a-real-hook": () => seen5.push("never"),
});
} catch (e) { threw5 = e; }
assert("B.5: subscribeMany with bad name throws", threw5 instanceof TypeError);
// Atomicity: bad-name batch should not register the good name.
Hooks.callAll("updateActor", { id: "a3" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.5: subscribeMany is atomic — nothing was registered", seen5, []);
// B.5b subscribeMany happy path.
const seen5b = [];
const u5 = subscribeMany({
updateActor: (env) => seen5b.push("ua:" + env.hook),
createToken: (env) => seen5b.push("ct:" + env.hook),
});
Hooks.callAll("updateActor", { id: "a4" }, {}, {}, "u1");
Hooks.callAll("createToken", { id: "t1" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.5b: subscribeMany dispatches both hooks", seen5b.sort(), ["ct:createToken", "ua:updateActor"]);
u5();
Hooks.callAll("updateActor", { id: "a5" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.5b: subscribeMany unsubscribe removes both", seen5b.sort(), ["ct:createToken", "ua:updateActor"]);
// B.6 subscribeAll.
unsubscribeAll();
const seen6 = [];
const u6 = subscribeAll((env) => seen6.push(env.hook));
Hooks.callAll("updateActor", { id: "a6" }, {}, {}, "u1");
Hooks.callAll("createToken", { id: "t2" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.6: subscribeAll receives every envelope", seen6.sort(), ["createToken", "updateActor"]);
u6();
Hooks.callAll("updateActor", { id: "a7" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.6: subscribeAll unsubscribe removes", seen6.sort(), ["createToken", "updateActor"]);
// B.7 unsubscribeAll.
unsubscribeAll();
const seen7 = [];
subscribe("updateActor", () => seen7.push("x"));
subscribe("createToken", () => seen7.push("y"));
unsubscribeAll();
Hooks.callAll("updateActor", { id: "a8" }, {}, {}, "u1");
Hooks.callAll("createToken", { id: "t3" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("B.7: unsubscribeAll purges everything", seen7, []);
// ----- Section C — Error containment -----
uninstall();
install();
unsubscribeAll();
console.log("[C] Error containment");
const seenC = [];
subscribe("updateActor", () => { throw new Error("boom-1"); });
subscribe("updateActor", (env) => seenC.push(env.hook));
const consoleErrorCalls = [];
const origConsoleError = console.error;
console.error = (...args) => consoleErrorCalls.push(args);
try {
Hooks.callAll("updateActor", { id: "a9" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
} finally {
console.error = origConsoleError;
}
assertEq("C: second callback still fires after first throws", seenC, ["updateActor"]);
assert(
"C: error logged via console.error with [foundry-hooks-lib] prefix and hook name",
consoleErrorCalls.length > 0 &&
consoleErrorCalls.some((args) =>
String(args[0]).includes("[foundry-hooks-lib]") &&
String(args[0]).includes("updateActor")
)
);
// ----- Section D — Lifecycle -----
uninstall();
install();
console.log("[D] Lifecycle");
// D.1: install registers Foundry hooks.
const installed = listInstalledRawHooks();
assert(
"D.1: install registers many raw Foundry hooks",
installed.length >= 50,
`got ${installed.length}`
);
assert(
"D.1: install registers updateActor",
installed.includes("updateActor")
);
assert(
"D.1: install registers combatStart",
installed.includes("combatStart")
);
// Idempotent: install() again does not double-register.
install();
assertEq("D.1: install is idempotent", listInstalledRawHooks().length, installed.length);
// D.2: ready evaluates adapters. With a dnd5e 5.2.5 system, a
// registered dnd5e adapter whose range covers 5.2.5 should load.
setGameSystem({ id: "dnd5e", version: "5.2.5" });
setGameVersion("13.351.0");
let factoryCalled = false;
resetAdapters();
registerSystemAdapter({
id: "test-dnd5e",
moduleId: "test-dnd5e",
system: { id: "dnd5e", versions: ">=5.2.0 <5.3.0" },
foundryVersions: ">=13 <15",
factory: () => {
factoryCalled = true;
return [{ name: "test.event", register: () => {} }];
},
});
evaluateAdaptersAtReady();
assert("D.2: matching adapter is loaded", factoryCalled);
assertEq("D.2: active adapters = 1", listActiveAdapters().length, 1);
// D.3: non-matching system silently skipped.
resetAdapters();
registerSystemAdapter({
id: "test-pf2e",
moduleId: "test-pf2e",
system: { id: "pf2e", versions: ">=4.0.0" },
foundryVersions: ">=13 <15",
factory: () => { factoryCalled = true; return []; },
});
factoryCalled = false;
evaluateAdaptersAtReady();
assert("D.3: non-matching system silently skipped (factory NOT called)", !factoryCalled);
// D.4: version mismatch logs + skips.
resetAdapters();
factoryCalled = false;
registerSystemAdapter({
id: "test-dnd5e-old",
moduleId: "test-dnd5e-old",
system: { id: "dnd5e", versions: ">=5.1.0 <5.2.0" },
foundryVersions: ">=13 <15",
factory: () => { factoryCalled = true; return []; },
});
const consoleWarnCalls = [];
const origConsoleWarn = console.warn;
console.warn = (...args) => consoleWarnCalls.push(args);
try {
evaluateAdaptersAtReady();
} finally {
console.warn = origConsoleWarn;
}
assert("D.4: version-mismatched adapter skipped (factory NOT called)", !factoryCalled);
assert(
"D.4: warning logged naming the adapter and version",
consoleWarnCalls.some((args) =>
String(args.join(" ")).includes("test-dnd5e-old") &&
String(args.join(" ")).includes("5.2.5") &&
String(args.join(" ")).includes("5.1.0")
)
);
// D.5: throwing factory is contained.
resetAdapters();
registerSystemAdapter({
id: "test-throws",
moduleId: "test-throws",
system: { id: "dnd5e", versions: "*" },
foundryVersions: "*",
factory: () => { throw new Error("factory boom"); },
});
registerSystemAdapter({
id: "test-ok",
moduleId: "test-ok",
system: { id: "dnd5e", versions: "*" },
foundryVersions: "*",
factory: () => [],
});
const consoleErrorCalls2 = [];
console.error = (...args) => consoleErrorCalls2.push(args);
try {
evaluateAdaptersAtReady();
} finally {
console.error = origConsoleError;
}
assertEq("D.5: failing adapter marked failed", listFailedAdapters().length, 1);
assertEq("D.5: failing adapter did NOT become active", listFailedAdapters().includes("test-throws"), true);
// OK adapter should still have loaded.
const activeAfter = listActiveAdapters();
assertEq("D.5: ok adapter still loads despite sibling failure", activeAfter.length, 1);
assertEq("D.5: ok adapter is the surviving one", activeAfter[0].id, "test-ok");
// D.6: uninstall removes all Foundry listeners.
uninstall();
assertEq("D.6: uninstall removes all registered listeners", listInstalledRawHooks().length, 0);
// Re-fire a Foundry hook — should produce NO envelopes.
unsubscribeAll();
let receivedD6 = null;
subscribe("updateActor", (env) => { receivedD6 = env; });
Hooks.callAll("updateActor", { id: "after-uninstall" }, {}, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assert("D.6: after uninstall, Foundry fires produce no envelopes", receivedD6 === null);
// ----- Section E — Anti-corruption -----
install();
console.log("[E] Anti-corruption");
// E.1: combatInactive synthesized from updateCombat(active→false).
let inactiveSeen = null;
const uE1 = subscribe("combatInactive", (env) => { inactiveSeen = env; });
const fakeCombat = { id: "c1", active: true };
Hooks.callAll("updateCombat", fakeCombat, { active: false }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assert("E.1: combatInactive synthesized from updateCombat active→false", inactiveSeen !== null);
assertEq("E.1: combatInactive.args[0] is the combat", inactiveSeen?.args?.[0]?.id, "c1");
uE1();
// E.1b: updateCombat with active=true should NOT synthesize.
let inactiveSeenB = null;
const uE1b = subscribe("combatInactive", (env) => { inactiveSeenB = env; });
Hooks.callAll("updateCombat", fakeCombat, { active: true }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assert("E.1b: updateCombat active=true does NOT synthesize combatInactive", inactiveSeenB === null);
uE1b();
// E.1c: updateCombat with no `active` change should NOT synthesize.
let inactiveSeenC = null;
const uE1c = subscribe("combatInactive", (env) => { inactiveSeenC = env; });
Hooks.callAll("updateCombat", fakeCombat, { round: 2 }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assert("E.1c: updateCombat round-change does NOT synthesize combatInactive", inactiveSeenC === null);
uE1c();
// E.2: renderChatInput is registered for BOTH v13 (renderChatLog) and v14 names.
assert(
"E.2: renderChatLog (v13) is in installed raw hooks",
listInstalledRawHooks().includes("renderChatLog")
);
assert(
"E.2: renderChatInput (v14) is in installed raw hooks",
listInstalledRawHooks().includes("renderChatInput")
);
// E.3: both produce envelope.hook === "renderChatInput".
let chatInputSeen = [];
const uE3 = subscribe("renderChatInput", (env) => chatInputSeen.push(env.hook));
Hooks.callAll("renderChatInput", { id: "m1" }, {}, "u1");
Hooks.callAll("renderChatLog", { id: "m2" }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("E.3: both v13 and v14 chat hooks produce envelope.hook='renderChatInput'", chatInputSeen, ["renderChatInput", "renderChatInput"]);
uE3();
// E.4: arg normalization. combatRound v13 shape: (combat, updateData, roundNum).
// v14 shape: (combat, updateData, updateOptions). Normalized to 4 args.
let roundArgsV13 = null;
const uE4a = subscribe("combatRound", (env) => { roundArgsV13 = env.args; });
Hooks.callAll("combatRound", { id: "c1" }, {}, 3, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("E.4a: combatRound v13 shape normalizes round num to args[2]", roundArgsV13?.[2], 3);
uE4a();
let roundArgsV14 = null;
const uE4b = subscribe("combatRound", (env) => { roundArgsV14 = env.args; });
Hooks.callAll("combatRound", { id: "c1" }, {}, { round: 5 }, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("E.4b: combatRound v14 shape normalizes round num to args[2]", roundArgsV14?.[2], 5);
uE4b();
// ----- Section G — System adapter loading deeper -----
unsubscribeAll();
resetAdapters();
console.log("[G] Adapter loading (deeper)");
// G.1: invalid manifest throws synchronously.
let threwG1 = null;
try { registerSystemAdapter(null); } catch (e) { threwG1 = e; }
assert("G.1: null manifest throws TypeError", threwG1 instanceof TypeError);
// G.2: manifest with no factory throws.
let threwG2 = null;
try { registerSystemAdapter({ id: "x", moduleId: "x", system: { id: "dnd5e" } }); } catch (e) { threwG2 = e; }
assert("G.2: manifest without factory throws TypeError", threwG2 instanceof TypeError);
// G.3: factory returning non-array fails the adapter.
registerSystemAdapter({
id: "test-non-array",
moduleId: "test-non-array",
system: { id: "dnd5e", versions: "*" },
foundryVersions: "*",
factory: () => "not-an-array",
});
console.error = () => {};
evaluateAtReady({ id: "dnd5e", version: "5.2.5" }, "13.351.0");
assert("G.3: factory returning non-array marks adapter failed", listFailedAdapters().includes("test-non-array"));
assert("G.3: factory returning non-array does NOT make adapter active", !listActiveAdapters().some((m) => m.id === "test-non-array"));
// G.4: duplicate id is a no-op with warning.
resetAdapters();
const consoleWarnG4 = [];
console.warn = (...args) => consoleWarnG4.push(args);
registerSystemAdapter({
id: "dup", moduleId: "dup",
system: { id: "dnd5e", versions: "*" }, foundryVersions: "*",
factory: () => [],
});
registerSystemAdapter({
id: "dup", moduleId: "dup-2",
system: { id: "dnd5e", versions: "*" }, foundryVersions: "*",
factory: () => [],
});
console.warn = origConsoleWarn;
assert(
"G.4: duplicate id registration is a no-op with warning",
consoleWarnG4.some((args) => String(args[0]).includes("dup") && String(args[0]).includes("already registered"))
);
// ----- Section D.7 — World change idempotency -----
console.log("[D.7] World change idempotency");
resetAdapters();
let callsD7 = 0;
registerSystemAdapter({
id: "d7", moduleId: "d7",
system: { id: "dnd5e", versions: "*" }, foundryVersions: "*",
factory: () => { callsD7++; return []; },
});
console.error = () => {};
evaluateAtReady({ id: "dnd5e", version: "5.2.5" }, "13.351.0");
evaluateAtReady({ id: "dnd5e", version: "5.2.5" }, "13.351.0");
// Re-evaluation calls factory again. This is the documented behavior
// in §4.2 — adapters must be idempotent.
// v0.3.0 implementation re-evaluates on each ready. Adapters must
// handle this. We assert that the factory IS called twice (re-eval
// happened), since the adapter protocol requires idempotency.
assert("D.7: re-evaluation calls factory (adapter must be idempotent)", callsD7 === 2);
// ----- Summary -----
console.error = origConsoleError;
console.warn = origConsoleWarn;
const passed = ASSERTIONS.filter((a) => a.pass).length;
const total = ASSERTIONS.length;
console.log(`\n--- ${passed}/${total} assertions passed ---`);
if (passed !== total) {
console.log("\nFailed assertions:");
for (const a of ASSERTIONS.filter((x) => !x.pass)) {
console.log(`${a.name} ${a.extra}`);
}
process.exitCode = 1;
}
}
// Helper: build plausible synthetic args for any registered raw hook.
function syntheticArgsFor(rawName) {
if (rawName === "combatStart") return [{ id: "c1", scene: { name: "S" } }, {}];
if (rawName === "combatEnd") return [{ id: "c1" }];
if (rawName === "combatTurn") return [{ id: "c1" }, {}, {}];
if (rawName === "combatRound") return [{ id: "c1" }, {}, 1];
if (rawName === "updateCombat") return [{ id: "c1", active: true }, { round: 2 }];
if (rawName.endsWith("Actor") || rawName.endsWith("Token") || rawName.endsWith("Item") || rawName.endsWith("Scene") || rawName.endsWith("JournalEntry") || rawName.endsWith("ActiveEffect") || rawName.endsWith("Combat") || rawName.endsWith("Combatant")) {
return [{ id: "d1" }, { name: "x" }, {}, "u1"];
}
if (rawName === "pauseGame") return [false];
if (rawName === "canvasInit") return [{}];
if (rawName === "canvasReady") return [{}];
if (rawName === "canvasPan") return [{}, { x: 0, y: 0, scale: 1 }];
if (rawName === "controlToken" || rawName === "hoverToken") return [{}, true];
if (rawName === "targetToken") return [{}, {}, true];
if (rawName === "lightingRefresh" || rawName === "sightRefresh") return [{}];
if (rawName === "collapseSidebar" || rawName === "collapseSceneNavigation") return [{}, true];
if (rawName === "changeSidebarTab") return [{}];
if (rawName === "getSceneControlButtons") return [[]];
if (rawName === "renderChatMessage") return [{ id: "m1" }, {}, {}];
if (rawName === "renderChatInput" || rawName === "renderChatLog") return [{}, {}, {}];
if (rawName === "renderJournalPageSheet") return [{}, {}, {}];
if (rawName === "initializePointSourceShaders") return [{}];
if (rawName === "rtcSettingsChanged") return [{}, {}];
if (rawName === "dnd5e.rollAttackV2" || rawName === "dnd5e.rollDamageV2") return [[{ total: 15 }], { subject: {} }];
if (rawName === "init" || rawName === "setup" || rawName === "ready") return [];
if (rawName === "createChatMessage") return [{ id: "m1" }, {}, "u1"];
return [];
}
mainTest().catch((e) => {
console.error("[verify-hooks-lib] uncaught:", e);
console.error(e.stack);
process.exitCode = 1;
});