diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index f4cc84e98b6..afddf06b45d 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -393,6 +393,12 @@ const expectOkInvokeResponse = async (res: Response) => { return body as { ok: boolean; result?: Record }; }; +const firstHookCallArg = () => { + const call = hookMocks.runBeforeToolCallHook.mock.calls[0]; + expect(call).toBeDefined(); + return call?.[0] as RunBeforeToolCallHookArgs; +}; + const invokeToolsRpc = async (params: Record, 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"); }); });