Files
hooks-lib/tests/verify-hooks-lib.mjs
Kaysser Kayyali 2fabb5e98f v0.4.1: drop renderChatMessage, register renderChatMessageHTML
Foundry v13 deprecated renderChatMessage in favor of
renderChatMessageHTML (which passes an HTMLElement, not a jQuery
wrapper). Subscribing to the deprecated hook re-emits Foundry's
compatibility warning on every chat render in worlds still running
v13.351 (the foundry-hooks-lib module's tests run against such a
world).

v0.3.0 already narrowed scope to Foundry v14 only (HOOK_CONTRACT.md
section 9), but the registered hook set still included
renderChatMessage as a legacy fallback. There is no Foundry v14
hook by that name, so the entry was dead weight — and worse, any
v13.351 world running the v14-only library would still see the
deprecation warning every chat render.

Changes:
- registered-hooks.js: replace renderChatMessage entry with
  renderChatMessageHTML. Update arg shape (HTML passes HTMLElement,
  not jQuery). Add comment explaining the deprecation.
- README.md / HOOK_CONTRACT.md section 6: list renderChatMessageHTML
  instead of renderChatMessage.
- tests/verify-hooks-lib.mjs: update stub arg shape from
  [{id}, {}, {}] to [{id}, {}] (v14 signature).

Verification:
- node tests/verify-hooks-lib.mjs: 546/546 (unchanged)
- node tests/perf.mjs: 6/6, median 0.0003ms/fire (well under
  the 0.1ms budget in HOOK_CONTRACT.md section 7)
- node --check on all scripts + tests: clean

Push: Gitea only.

Note: battle-focus's own main.js line 144 still has a
Hooks.on('renderChatMessage', ...) listener for its 'Open in
Journal' button wiring. That listener fires the deprecation warning
on the user's console. Fixing it is a battle-focus change, out of
scope for this turn (hooks-lib only).
2026-06-20 22:49:32 -04:00

534 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 (v14 only; v13's renderChatLog
// was supported in v0.2.0 but is dropped in v0.3.0 per the plan's
// v14-only scope).
assert(
"E.2: renderChatInput (v14) is in installed raw hooks",
listInstalledRawHooks().includes("renderChatInput")
);
assert(
"E.2: renderChatLog (v13) is NOT in installed raw hooks",
!listInstalledRawHooks().includes("renderChatLog")
);
// E.3: v14 fire produces envelope.hook === "renderChatInput".
let chatInputSeen = [];
const uE3 = subscribe("renderChatInput", (env) => chatInputSeen.push(env.hook));
Hooks.callAll("renderChatInput", { id: "m1" }, {}, "u1");
await new Promise((r) => setTimeout(r, 10));
assertEq("E.3: v14 renderChatInput produces envelope.hook='renderChatInput'", chatInputSeen, ["renderChatInput"]);
uE3();
// E.4: arg normalization. v14 combatRound shape: (combat, updateData, updateOptions).
// v0.3.0 is v14-only; the v13 round-num position is no longer
// exercised. round number comes from updateOptions.round.
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.4: 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 === "renderChatMessageHTML") return [{ id: "m1" }, {}];
if (rawName === "renderChatInput") 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;
});