Files
openclaw/src/plugins/hooks.before-tool-call.test.ts
2026-03-28 03:30:25 +00:00

244 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { createHookRunner } from "./hooks.js";
import { addStaticTestHooks } from "./hooks.test-helpers.js";
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
import type { PluginHookToolContext } from "./types.js";
import type { PluginHookBeforeToolCallResult } from "./types.js";
const stubCtx: PluginHookToolContext = {
toolName: "bash",
agentId: "main",
sessionKey: "agent:main:main",
};
async function runBeforeToolCallWithHooks(
registry: PluginRegistry,
hooks: ReadonlyArray<{
pluginId: string;
result: PluginHookBeforeToolCallResult;
priority?: number;
}>,
) {
addStaticTestHooks(registry, {
hookName: "before_tool_call",
hooks,
});
const runner = createHookRunner(registry);
return await runner.runBeforeToolCall({ toolName: "bash", params: {} }, stubCtx);
}
function expectRequireApprovalResult(
result: PluginHookBeforeToolCallResult | undefined,
expected: {
block?: boolean;
blockReason?: string;
params?: Record<string, unknown>;
requireApproval?: Record<string, unknown>;
},
) {
expect(result?.block).toBe(expected.block);
expect(result?.blockReason).toBe(expected.blockReason);
expect(result?.params).toEqual(expected.params);
expect(result?.requireApproval).toEqual(
expected.requireApproval ? expect.objectContaining(expected.requireApproval) : undefined,
);
}
describe("before_tool_call hook merger — requireApproval", () => {
let registry: PluginRegistry;
beforeEach(() => {
registry = createEmptyPluginRegistry();
});
it.each([
{
name: "propagates requireApproval from a single plugin",
hooks: [
{
pluginId: "sage",
result: {
requireApproval: {
id: "approval-1",
title: "Sensitive tool",
description: "This tool does something sensitive",
severity: "warning",
},
},
},
],
expectedApproval: {
id: "approval-1",
title: "Sensitive tool",
description: "This tool does something sensitive",
severity: "warning",
pluginId: "sage",
},
},
{
name: "stamps pluginId from the registration",
hooks: [
{
pluginId: "my-plugin",
result: {
requireApproval: {
id: "a1",
title: "T",
description: "D",
},
},
},
],
expectedApproval: {
pluginId: "my-plugin",
},
},
{
name: "first hook with requireApproval wins when multiple plugins set it",
hooks: [
{
pluginId: "plugin-a",
result: {
requireApproval: {
title: "First",
description: "First plugin",
},
},
priority: 100,
},
{
pluginId: "plugin-b",
result: {
requireApproval: {
title: "Second",
description: "Second plugin",
},
},
priority: 50,
},
],
expectedApproval: {
title: "First",
pluginId: "plugin-a",
},
},
{
name: "does not overwrite pluginId if plugin sets it (stamped by merger)",
hooks: [
{
pluginId: "actual-plugin",
result: {
requireApproval: {
title: "T",
description: "D",
pluginId: "should-be-overwritten",
},
},
},
],
expectedApproval: {
pluginId: "actual-plugin",
},
},
] as const)("$name", async ({ hooks, expectedApproval }) => {
const result = await runBeforeToolCallWithHooks(registry, hooks);
expectRequireApprovalResult(result, { requireApproval: expectedApproval });
});
it("merges block and requireApproval from different plugins", async () => {
const result = await runBeforeToolCallWithHooks(registry, [
{
pluginId: "approver",
result: {
requireApproval: {
title: "Needs approval",
description: "Approval needed",
},
},
priority: 100,
},
{
pluginId: "blocker",
result: {
block: true,
blockReason: "blocked",
},
priority: 50,
},
]);
expect(result?.block).toBe(true);
expect(result?.requireApproval?.title).toBe("Needs approval");
});
it("returns undefined requireApproval when no plugin sets it", async () => {
const result = await runBeforeToolCallWithHooks(registry, [
{ pluginId: "plain", result: { params: { extra: true } } },
]);
expect(result?.requireApproval).toBeUndefined();
});
it.each([
{
name: "freezes params after requireApproval when a lower-priority plugin tries to override them",
hooks: [
{
pluginId: "approver",
result: {
params: { source: "approver", safe: true },
requireApproval: {
title: "Needs approval",
description: "Approval needed",
},
},
priority: 100,
},
{
pluginId: "mutator",
result: {
params: { source: "mutator", safe: false },
},
priority: 50,
},
],
expected: {
requireApproval: { pluginId: "approver" },
params: { source: "approver", safe: true },
},
},
{
name: "still allows block=true from a lower-priority plugin after requireApproval",
hooks: [
{
pluginId: "approver",
result: {
params: { source: "approver", safe: true },
requireApproval: {
title: "Needs approval",
description: "Approval needed",
},
},
priority: 100,
},
{
pluginId: "blocker",
result: {
block: true,
blockReason: "blocked",
params: { source: "blocker", safe: false },
},
priority: 50,
},
],
expected: {
block: true,
blockReason: "blocked",
requireApproval: { pluginId: "approver" },
params: { source: "approver", safe: true },
},
},
] as const)("$name", async ({ hooks, expected }) => {
const result = await runBeforeToolCallWithHooks(registry, hooks);
expectRequireApprovalResult(result, expected);
});
});