Skip to content

Commit 12702e1

Browse files
authored
plugins: harden global hook runner state (openclaw#40184)
1 parent 14bbcad commit 12702e1

File tree

2 files changed

+74
-9
lines changed

2 files changed

+74
-9
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { createMockPluginRegistry } from "./hooks.test-helpers.js";
3+
4+
async function importHookRunnerGlobalModule() {
5+
return import("./hook-runner-global.js");
6+
}
7+
8+
afterEach(async () => {
9+
const mod = await importHookRunnerGlobalModule();
10+
mod.resetGlobalHookRunner();
11+
vi.resetModules();
12+
});
13+
14+
describe("hook-runner-global", () => {
15+
it("preserves the initialized runner across module reloads", async () => {
16+
const modA = await importHookRunnerGlobalModule();
17+
const registry = createMockPluginRegistry([{ hookName: "message_received", handler: vi.fn() }]);
18+
19+
modA.initializeGlobalHookRunner(registry);
20+
expect(modA.getGlobalHookRunner()?.hasHooks("message_received")).toBe(true);
21+
22+
vi.resetModules();
23+
24+
const modB = await importHookRunnerGlobalModule();
25+
expect(modB.getGlobalHookRunner()).not.toBeNull();
26+
expect(modB.getGlobalHookRunner()?.hasHooks("message_received")).toBe(true);
27+
expect(modB.getGlobalPluginRegistry()).toBe(registry);
28+
});
29+
30+
it("clears the shared state across module reloads", async () => {
31+
const modA = await importHookRunnerGlobalModule();
32+
const registry = createMockPluginRegistry([{ hookName: "message_received", handler: vi.fn() }]);
33+
34+
modA.initializeGlobalHookRunner(registry);
35+
36+
vi.resetModules();
37+
38+
const modB = await importHookRunnerGlobalModule();
39+
modB.resetGlobalHookRunner();
40+
expect(modB.getGlobalHookRunner()).toBeNull();
41+
expect(modB.getGlobalPluginRegistry()).toBeNull();
42+
43+
vi.resetModules();
44+
45+
const modC = await importHookRunnerGlobalModule();
46+
expect(modC.getGlobalHookRunner()).toBeNull();
47+
expect(modC.getGlobalPluginRegistry()).toBeNull();
48+
});
49+
});

src/plugins/hook-runner-global.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,31 @@ import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./typ
1212

1313
const log = createSubsystemLogger("plugins");
1414

15-
let globalHookRunner: HookRunner | null = null;
16-
let globalRegistry: PluginRegistry | null = null;
15+
type HookRunnerGlobalState = {
16+
hookRunner: HookRunner | null;
17+
registry: PluginRegistry | null;
18+
};
19+
20+
const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state");
21+
22+
function getHookRunnerGlobalState(): HookRunnerGlobalState {
23+
const globalStore = globalThis as typeof globalThis & {
24+
[hookRunnerGlobalStateKey]?: HookRunnerGlobalState;
25+
};
26+
return (globalStore[hookRunnerGlobalStateKey] ??= {
27+
hookRunner: null,
28+
registry: null,
29+
});
30+
}
1731

1832
/**
1933
* Initialize the global hook runner with a plugin registry.
2034
* Called once when plugins are loaded during gateway startup.
2135
*/
2236
export function initializeGlobalHookRunner(registry: PluginRegistry): void {
23-
globalRegistry = registry;
24-
globalHookRunner = createHookRunner(registry, {
37+
const state = getHookRunnerGlobalState();
38+
state.registry = registry;
39+
state.hookRunner = createHookRunner(registry, {
2540
logger: {
2641
debug: (msg) => log.debug(msg),
2742
warn: (msg) => log.warn(msg),
@@ -41,22 +56,22 @@ export function initializeGlobalHookRunner(registry: PluginRegistry): void {
4156
* Returns null if plugins haven't been loaded yet.
4257
*/
4358
export function getGlobalHookRunner(): HookRunner | null {
44-
return globalHookRunner;
59+
return getHookRunnerGlobalState().hookRunner;
4560
}
4661

4762
/**
4863
* Get the global plugin registry.
4964
* Returns null if plugins haven't been loaded yet.
5065
*/
5166
export function getGlobalPluginRegistry(): PluginRegistry | null {
52-
return globalRegistry;
67+
return getHookRunnerGlobalState().registry;
5368
}
5469

5570
/**
5671
* Check if any hooks are registered for a given hook name.
5772
*/
5873
export function hasGlobalHooks(hookName: Parameters<HookRunner["hasHooks"]>[0]): boolean {
59-
return globalHookRunner?.hasHooks(hookName) ?? false;
74+
return getHookRunnerGlobalState().hookRunner?.hasHooks(hookName) ?? false;
6075
}
6176

6277
export async function runGlobalGatewayStopSafely(params: {
@@ -83,6 +98,7 @@ export async function runGlobalGatewayStopSafely(params: {
8398
* Reset the global hook runner (for testing).
8499
*/
85100
export function resetGlobalHookRunner(): void {
86-
globalHookRunner = null;
87-
globalRegistry = null;
101+
const state = getHookRunnerGlobalState();
102+
state.hookRunner = null;
103+
state.registry = null;
88104
}

0 commit comments

Comments
 (0)