test: clear gateway tools invoke broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 16:48:55 +01:00
parent 83390ad0d5
commit 147bf4807b

View File

@@ -393,6 +393,12 @@ const expectOkInvokeResponse = async (res: Response) => {
return body as { ok: boolean; result?: Record<string, unknown> };
};
const firstHookCallArg = () => {
const call = hookMocks.runBeforeToolCallHook.mock.calls[0];
expect(call).toBeDefined();
return call?.[0] as RunBeforeToolCallHookArgs;
};
const invokeToolsRpc = async (params: Record<string, unknown>, scopes = ["operator.write"]) => {
const respond = vi.fn();
await toolsInvokeHandlers["tools.invoke"]({
@@ -442,17 +448,14 @@ describe("POST /tools/invoke", () => {
expect(body).toHaveProperty("result");
expect(lastCreateOpenClawToolsContext?.allowMediaInvokeCommands).toBe(true);
expect(lastCreateOpenClawToolsContext?.disablePluginTools).toBe(true);
expect(hookMocks.runBeforeToolCallHook).toHaveBeenCalledWith(
expect.objectContaining({
toolName: "agents_list",
ctx: expect.objectContaining({
agentId: "main",
config: cfg,
sessionKey: "agent:main:main",
loopDetection: { warnAt: 3 },
}),
}),
);
const hookArg = firstHookCallArg();
expect(hookArg.toolName).toBe("agents_list");
const hookCtx = hookArg.ctx;
expect(hookCtx).toBeDefined();
expect(hookCtx?.agentId).toBe("main");
expect(hookCtx?.config).toBe(cfg);
expect(hookCtx?.sessionKey).toBe("agent:main:main");
expect(hookCtx?.loopDetection).toEqual({ warnAt: 3 });
});
it("opts direct gateway tool invocation into gateway subagent binding", async () => {
@@ -489,10 +492,9 @@ describe("POST /tools/invoke", () => {
});
const body = await expectOkInvokeResponse(res);
expect(body.result).toMatchObject({ ok: true, permissionFlow: true });
expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).toEqual(
expect.arrayContaining(["plugin_doctor"]),
);
expect(body.result?.ok).toBe(true);
expect(body.result?.permissionFlow).toBe(true);
expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).toContain("plugin_doctor");
});
it("uses tools.alsoAllow for optional plugin discovery without loading every plugin tool", async () => {
@@ -508,10 +510,9 @@ describe("POST /tools/invoke", () => {
});
const body = await expectOkInvokeResponse(res);
expect(body.result).toMatchObject({ ok: true, permissionFlow: true });
expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).toEqual(
expect.arrayContaining(["plugin_doctor"]),
);
expect(body.result?.ok).toBe(true);
expect(body.result?.permissionFlow).toBe(true);
expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).toContain("plugin_doctor");
expect(lastCreateOpenClawToolsContext?.pluginToolAllowlist).not.toContain("*");
});
@@ -529,13 +530,10 @@ describe("POST /tools/invoke", () => {
});
expect(res.status).toBe(403);
await expect(res.json()).resolves.toMatchObject({
ok: false,
error: {
type: "tool_call_blocked",
message: "blocked by test hook",
},
});
const body = await res.json();
expect(body.ok).toBe(false);
expect(body.error?.type).toBe("tool_call_blocked");
expect(body.error?.message).toBe("blocked by test hook");
});
it("accepts shared-secret bearer auth on the HTTP tools surface", async () => {
@@ -587,7 +585,7 @@ describe("POST /tools/invoke", () => {
});
const body = await expectOkInvokeResponse(res);
expect(body.result).toMatchObject({ ok: true });
expect(body.result?.ok).toBe(true);
});
it("supports tools.alsoAllow in profile and implicit modes", async () => {
@@ -866,13 +864,10 @@ describe("POST /tools/invoke", () => {
});
expect(res.status).toBe(403);
await expect(res.json()).resolves.toMatchObject({
ok: false,
error: {
type: "forbidden",
message: "missing scope: operator.write",
},
});
const body = await res.json();
expect(body.ok).toBe(false);
expect(body.error?.type).toBe("forbidden");
expect(body.error?.message).toBe("missing scope: operator.write");
});
it("treats shared-secret bearer auth as full operator access on /tools/invoke", async () => {
@@ -982,25 +977,20 @@ describe("tools.invoke Gateway RPC", () => {
});
expect(call?.[0]).toBe(true);
expect(call?.[1]).toMatchObject({
ok: true,
toolName: "agents_list",
output: { ok: true, result: [] },
source: "core",
});
expect(call?.[1]?.ok).toBe(true);
expect(call?.[1]?.toolName).toBe("agents_list");
expect(call?.[1]?.output).toEqual({ ok: true, result: [] });
expect((call?.[1] as { source?: unknown } | undefined)?.source).toBe("core");
expect(lastCreateOpenClawToolsContext?.allowGatewaySubagentBinding).toBe(true);
expect(hookMocks.runBeforeToolCallHook).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: "report",
toolName: "agents_list",
toolCallId: "rpc-rpc-tool-test",
ctx: expect.objectContaining({
agentId: "main",
config: cfg,
sessionKey: "agent:main:main",
}),
}),
);
const hookArg = firstHookCallArg();
expect(hookArg.approvalMode).toBe("report");
expect(hookArg.toolName).toBe("agents_list");
expect(hookArg.toolCallId).toBe("rpc-rpc-tool-test");
const hookCtx = hookArg.ctx;
expect(hookCtx).toBeDefined();
expect(hookCtx?.agentId).toBe("main");
expect(hookCtx?.config).toBe(cfg);
expect(hookCtx?.sessionKey).toBe("agent:main:main");
});
it("returns typed approval-needed refusal when the policy hook blocks", async () => {
@@ -1020,15 +1010,12 @@ describe("tools.invoke Gateway RPC", () => {
});
expect(call?.[0]).toBe(true);
expect(call?.[1]).toMatchObject({
ok: false,
toolName: "tools_invoke_test",
requiresApproval: true,
error: {
code: "requires_approval",
message: "Plugin approval required",
},
});
expect(call?.[1]?.ok).toBe(false);
expect(call?.[1]?.toolName).toBe("tools_invoke_test");
expect((call?.[1] as { requiresApproval?: unknown } | undefined)?.requiresApproval).toBe(true);
const error = call?.[1]?.error as { code?: string; message?: string } | undefined;
expect(error?.code).toBe("requires_approval");
expect(error?.message).toBe("Plugin approval required");
});
it("rejects mismatched session and agent scope", async () => {
@@ -1048,23 +1035,19 @@ describe("tools.invoke Gateway RPC", () => {
});
expect(call?.[0]).toBe(true);
expect(call?.[1]).toMatchObject({
ok: false,
toolName: "agents_list",
error: {
code: "validation_error",
message: 'agent id "other" does not match session agent "main"',
},
});
expect(call?.[1]?.ok).toBe(false);
expect(call?.[1]?.toolName).toBe("agents_list");
const error = call?.[1]?.error as { code?: string; message?: string } | undefined;
expect(error?.code).toBe("validation_error");
expect(error?.message).toBe('agent id "other" does not match session agent "main"');
});
it("rejects malformed params at the RPC boundary", async () => {
const call = await invokeToolsRpc({ name: "" });
expect(call?.[0]).toBe(false);
expect(call?.[2]).toMatchObject({
code: "INVALID_REQUEST",
message: expect.stringContaining("invalid tools.invoke params"),
});
const error = call?.[2] as { code?: string; message?: string } | undefined;
expect(error?.code).toBe("INVALID_REQUEST");
expect(error?.message).toContain("invalid tools.invoke params");
});
});