diff --git a/src/plugins/hook-runner-global.test.ts b/src/plugins/hook-runner-global.test.ts new file mode 100644 index 00000000000..8089feff430 --- /dev/null +++ b/src/plugins/hook-runner-global.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMockPluginRegistry } from "./hooks.test-helpers.js"; + +async function importHookRunnerGlobalModule() { + return import("./hook-runner-global.js"); +} + +afterEach(async () => { + const mod = await importHookRunnerGlobalModule(); + mod.resetGlobalHookRunner(); + vi.resetModules(); +}); + +describe("hook-runner-global", () => { + it("preserves the initialized runner across module reloads", async () => { + const modA = await importHookRunnerGlobalModule(); + const registry = createMockPluginRegistry([{ hookName: "message_received", handler: vi.fn() }]); + + modA.initializeGlobalHookRunner(registry); + expect(modA.getGlobalHookRunner()?.hasHooks("message_received")).toBe(true); + + vi.resetModules(); + + const modB = await importHookRunnerGlobalModule(); + expect(modB.getGlobalHookRunner()).not.toBeNull(); + expect(modB.getGlobalHookRunner()?.hasHooks("message_received")).toBe(true); + expect(modB.getGlobalPluginRegistry()).toBe(registry); + }); + + it("clears the shared state across module reloads", async () => { + const modA = await importHookRunnerGlobalModule(); + const registry = createMockPluginRegistry([{ hookName: "message_received", handler: vi.fn() }]); + + modA.initializeGlobalHookRunner(registry); + + vi.resetModules(); + + const modB = await importHookRunnerGlobalModule(); + modB.resetGlobalHookRunner(); + expect(modB.getGlobalHookRunner()).toBeNull(); + expect(modB.getGlobalPluginRegistry()).toBeNull(); + + vi.resetModules(); + + const modC = await importHookRunnerGlobalModule(); + expect(modC.getGlobalHookRunner()).toBeNull(); + expect(modC.getGlobalPluginRegistry()).toBeNull(); + }); +}); diff --git a/src/plugins/hook-runner-global.ts b/src/plugins/hook-runner-global.ts index 609721fcb4d..b2613f3467f 100644 --- a/src/plugins/hook-runner-global.ts +++ b/src/plugins/hook-runner-global.ts @@ -12,16 +12,31 @@ import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./typ const log = createSubsystemLogger("plugins"); -let globalHookRunner: HookRunner | null = null; -let globalRegistry: PluginRegistry | null = null; +type HookRunnerGlobalState = { + hookRunner: HookRunner | null; + registry: PluginRegistry | null; +}; + +const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state"); + +function getHookRunnerGlobalState(): HookRunnerGlobalState { + const globalStore = globalThis as typeof globalThis & { + [hookRunnerGlobalStateKey]?: HookRunnerGlobalState; + }; + return (globalStore[hookRunnerGlobalStateKey] ??= { + hookRunner: null, + registry: null, + }); +} /** * Initialize the global hook runner with a plugin registry. * Called once when plugins are loaded during gateway startup. */ export function initializeGlobalHookRunner(registry: PluginRegistry): void { - globalRegistry = registry; - globalHookRunner = createHookRunner(registry, { + const state = getHookRunnerGlobalState(); + state.registry = registry; + state.hookRunner = createHookRunner(registry, { logger: { debug: (msg) => log.debug(msg), warn: (msg) => log.warn(msg), @@ -41,7 +56,7 @@ export function initializeGlobalHookRunner(registry: PluginRegistry): void { * Returns null if plugins haven't been loaded yet. */ export function getGlobalHookRunner(): HookRunner | null { - return globalHookRunner; + return getHookRunnerGlobalState().hookRunner; } /** @@ -49,14 +64,14 @@ export function getGlobalHookRunner(): HookRunner | null { * Returns null if plugins haven't been loaded yet. */ export function getGlobalPluginRegistry(): PluginRegistry | null { - return globalRegistry; + return getHookRunnerGlobalState().registry; } /** * Check if any hooks are registered for a given hook name. */ export function hasGlobalHooks(hookName: Parameters[0]): boolean { - return globalHookRunner?.hasHooks(hookName) ?? false; + return getHookRunnerGlobalState().hookRunner?.hasHooks(hookName) ?? false; } export async function runGlobalGatewayStopSafely(params: { @@ -83,6 +98,7 @@ export async function runGlobalGatewayStopSafely(params: { * Reset the global hook runner (for testing). */ export function resetGlobalHookRunner(): void { - globalHookRunner = null; - globalRegistry = null; + const state = getHookRunnerGlobalState(); + state.hookRunner = null; + state.registry = null; }