plugins: harden global hook runner state (#40184)

This commit is contained in:
Vincent Koc
2026-03-09 11:20:33 -07:00
committed by GitHub
parent 14bbcad169
commit 12702e11a5
2 changed files with 74 additions and 9 deletions

View File

@@ -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();
});
});

View File

@@ -12,16 +12,31 @@ import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./typ
const log = createSubsystemLogger("plugins"); const log = createSubsystemLogger("plugins");
let globalHookRunner: HookRunner | null = null; type HookRunnerGlobalState = {
let globalRegistry: PluginRegistry | null = null; 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. * Initialize the global hook runner with a plugin registry.
* Called once when plugins are loaded during gateway startup. * Called once when plugins are loaded during gateway startup.
*/ */
export function initializeGlobalHookRunner(registry: PluginRegistry): void { export function initializeGlobalHookRunner(registry: PluginRegistry): void {
globalRegistry = registry; const state = getHookRunnerGlobalState();
globalHookRunner = createHookRunner(registry, { state.registry = registry;
state.hookRunner = createHookRunner(registry, {
logger: { logger: {
debug: (msg) => log.debug(msg), debug: (msg) => log.debug(msg),
warn: (msg) => log.warn(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. * Returns null if plugins haven't been loaded yet.
*/ */
export function getGlobalHookRunner(): HookRunner | null { 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. * Returns null if plugins haven't been loaded yet.
*/ */
export function getGlobalPluginRegistry(): PluginRegistry | null { export function getGlobalPluginRegistry(): PluginRegistry | null {
return globalRegistry; return getHookRunnerGlobalState().registry;
} }
/** /**
* Check if any hooks are registered for a given hook name. * Check if any hooks are registered for a given hook name.
*/ */
export function hasGlobalHooks(hookName: Parameters<HookRunner["hasHooks"]>[0]): boolean { export function hasGlobalHooks(hookName: Parameters<HookRunner["hasHooks"]>[0]): boolean {
return globalHookRunner?.hasHooks(hookName) ?? false; return getHookRunnerGlobalState().hookRunner?.hasHooks(hookName) ?? false;
} }
export async function runGlobalGatewayStopSafely(params: { export async function runGlobalGatewayStopSafely(params: {
@@ -83,6 +98,7 @@ export async function runGlobalGatewayStopSafely(params: {
* Reset the global hook runner (for testing). * Reset the global hook runner (for testing).
*/ */
export function resetGlobalHookRunner(): void { export function resetGlobalHookRunner(): void {
globalHookRunner = null; const state = getHookRunnerGlobalState();
globalRegistry = null; state.hookRunner = null;
state.registry = null;
} }