mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-01 08:33:44 +00:00
fix(plugins): preserve mutable hook context (#97281)
* fix(plugins): preserve mutable hook context * fix(plugins): satisfy hook context lint
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { applyBootstrapHookOverrides } from "../agents/bootstrap-hooks.js";
|
||||
import { listRegisteredAgentHarnesses } from "../agents/harness/registry.js";
|
||||
import type { WorkspaceBootstrapFile } from "../agents/workspace.js";
|
||||
import { resolveConfigEnvVars } from "../config/env-substitution.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import {
|
||||
@@ -2973,6 +2975,163 @@ module.exports = { id: "throws-after-import", register() {} };`,
|
||||
clearInternalHooks();
|
||||
});
|
||||
|
||||
it("preserves plugin hook context mutations for bootstrap hooks", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "hook-bootstrap-mutation",
|
||||
filename: "hook-bootstrap-mutation.cjs",
|
||||
body: `module.exports = {
|
||||
id: "hook-bootstrap-mutation",
|
||||
register(api) {
|
||||
api.registerHook(
|
||||
"agent:bootstrap",
|
||||
(event) => {
|
||||
event.context.bootstrapFiles = [
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: "/tmp/override-AGENTS.md",
|
||||
content: "override bootstrap rules",
|
||||
missing: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ name: "hook-bootstrap-mutation" },
|
||||
);
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
clearInternalHooks();
|
||||
|
||||
loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["hook-bootstrap-mutation"],
|
||||
},
|
||||
options: {
|
||||
onlyPluginIds: ["hook-bootstrap-mutation"],
|
||||
},
|
||||
});
|
||||
|
||||
const updated = await applyBootstrapHookOverrides({
|
||||
files: [
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: "/tmp/base-AGENTS.md",
|
||||
content: "base bootstrap rules",
|
||||
missing: false,
|
||||
} satisfies WorkspaceBootstrapFile,
|
||||
],
|
||||
workspaceDir: "/tmp",
|
||||
sessionKey: "agent:main:subagent:test-bootstrap",
|
||||
});
|
||||
|
||||
expect(updated).toEqual([
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: "/tmp/override-AGENTS.md",
|
||||
content: "override bootstrap rules",
|
||||
missing: false,
|
||||
},
|
||||
]);
|
||||
|
||||
clearInternalHooks();
|
||||
});
|
||||
|
||||
it("runs consecutive plugin hook handlers with shared mutable context but isolated plugin config", async () => {
|
||||
useNoBundledPlugins();
|
||||
const first = writePlugin({
|
||||
id: "hook-context-first",
|
||||
filename: "hook-context-first.cjs",
|
||||
body: `module.exports = {
|
||||
id: "hook-context-first",
|
||||
register(api) {
|
||||
api.registerHook(
|
||||
"gateway:startup",
|
||||
(event) => {
|
||||
event.messages.push("first-config=" + event.context.pluginConfig?.marker);
|
||||
event.context.note = "mutation-from-first";
|
||||
},
|
||||
{ name: "hook-context-first" },
|
||||
);
|
||||
},
|
||||
};`,
|
||||
});
|
||||
const second = writePlugin({
|
||||
id: "hook-context-second",
|
||||
filename: "hook-context-second.cjs",
|
||||
body: `module.exports = {
|
||||
id: "hook-context-second",
|
||||
register(api) {
|
||||
api.registerHook(
|
||||
"gateway:startup",
|
||||
(event) => {
|
||||
event.messages.push(
|
||||
"second-config=" + String(event.context.pluginConfig?.marker ?? "none"),
|
||||
);
|
||||
event.messages.push("note=" + String(event.context.note ?? "missing-note"));
|
||||
},
|
||||
{ name: "hook-context-second" },
|
||||
);
|
||||
},
|
||||
};`,
|
||||
});
|
||||
for (const plugin of [first, second]) {
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: plugin.id,
|
||||
configSchema: { type: "object" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
clearInternalHooks();
|
||||
|
||||
loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: first.dir,
|
||||
onlyPluginIds: ["hook-context-first", "hook-context-second"],
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [first.file, second.file] },
|
||||
allow: ["hook-context-first", "hook-context-second"],
|
||||
entries: {
|
||||
"hook-context-first": {
|
||||
config: {
|
||||
marker: "visible-to-first",
|
||||
},
|
||||
},
|
||||
"hook-context-second": {
|
||||
config: {
|
||||
marker: "visible-to-second",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const event = createInternalHookEvent("gateway", "startup", "gateway:startup");
|
||||
await triggerInternalHook(event);
|
||||
|
||||
expect(event.messages).toEqual([
|
||||
"first-config=visible-to-first",
|
||||
"second-config=visible-to-second",
|
||||
"note=mutation-from-first",
|
||||
]);
|
||||
expect(event.context).toEqual({
|
||||
note: "mutation-from-first",
|
||||
});
|
||||
|
||||
clearInternalHooks();
|
||||
});
|
||||
|
||||
it("rolls back global side effects when registration fails", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
|
||||
@@ -741,9 +741,21 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
}> = [];
|
||||
for (const event of normalizedEvents) {
|
||||
const wrappedHandler: typeof handler = async (evt) => {
|
||||
// Shallow-copy to avoid mutating the shared event object
|
||||
// passed to all handlers sequentially by triggerInternalHook
|
||||
return handler({ ...evt, context: { ...evt.context, pluginConfig } });
|
||||
const context = evt.context;
|
||||
const hadPluginConfig = Object.hasOwn(context, "pluginConfig");
|
||||
const previousPluginConfig = context.pluginConfig;
|
||||
// Internal hooks intentionally share one mutable context object across
|
||||
// handlers; only pluginConfig stays per-handler and is restored after.
|
||||
context.pluginConfig = pluginConfig;
|
||||
try {
|
||||
return await handler({ ...evt, context });
|
||||
} finally {
|
||||
if (hadPluginConfig) {
|
||||
context.pluginConfig = previousPluginConfig;
|
||||
} else {
|
||||
delete context.pluginConfig;
|
||||
}
|
||||
}
|
||||
};
|
||||
registerInternalHook(event, wrappedHandler);
|
||||
nextRegistrations.push({ event, handler: wrappedHandler });
|
||||
|
||||
Reference in New Issue
Block a user