From c192604e36a22ba8b44fa7cc0b5689b5ecc6e320 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 12 May 2026 02:06:27 +0100 Subject: [PATCH] test: guard slack action mock calls --- extensions/slack/src/action-runtime.test.ts | 80 +++++++++++++++------ 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/extensions/slack/src/action-runtime.test.ts b/extensions/slack/src/action-runtime.test.ts index e9610ce17108..a2938cec8b2e 100644 --- a/extensions/slack/src/action-runtime.test.ts +++ b/extensions/slack/src/action-runtime.test.ts @@ -63,6 +63,39 @@ describe("handleSlackAction", () => { return value; } + function requireMockCall( + source: { mock: { calls: unknown[][] } }, + label: string, + index = 0, + ): unknown[] { + const call = source.mock.calls[index]; + if (!call) { + throw new Error(`missing ${label} call ${index + 1}`); + } + return call; + } + + function requireMockArg( + source: { mock: { calls: unknown[][] } }, + label: string, + callIndex: number, + argIndex: number, + ): unknown { + return requireMockCall(source, label, callIndex)[argIndex]; + } + + function requireRecordArg( + source: { mock: { calls: unknown[][] } }, + label: string, + callIndex: number, + argIndex: number, + ): Record { + return requireRecord( + requireMockArg(source, label, callIndex, argIndex), + `${label} call ${callIndex + 1} argument ${argIndex + 1}`, + ); + } + function expectRecordFields(record: Record, fields: Record) { for (const [key, value] of Object.entries(fields)) { expect(record[key]).toEqual(value); @@ -119,15 +152,15 @@ describe("handleSlackAction", () => { readSlackMessages.mockClear(); readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false }); await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg); - const opts = readSlackMessages.mock.calls[0]?.[1] as { token?: string } | undefined; - return opts?.token; + const token = requireRecordArg(readSlackMessages, "readSlackMessages", 0, 1).token; + return typeof token === "string" ? token : undefined; } async function resolveSendToken(cfg: OpenClawConfig): Promise { sendSlackMessage.mockClear(); await handleSlackAction({ action: "sendMessage", to: "channel:C1", content: "Hello" }, cfg); - const opts = sendSlackMessage.mock.calls[0]?.[2] as { token?: string } | undefined; - return opts?.token; + const token = requireRecordArg(sendSlackMessage, "sendSlackMessage", 0, 2).token; + return typeof token === "string" ? token : undefined; } beforeEach(() => { @@ -279,8 +312,8 @@ describe("handleSlackAction", () => { }, slackConfig(), ); - expect(downloadSlackFile.mock.calls[0]?.[0]).toBe("F123"); - expect(requireRecord(downloadSlackFile.mock.calls[0]?.[1], "download options").maxBytes).toBe( + expect(requireMockArg(downloadSlackFile, "downloadSlackFile", 0, 0)).toBe("F123"); + expect(requireRecordArg(downloadSlackFile, "downloadSlackFile", 0, 1).maxBytes).toBe( 20 * 1024 * 1024, ); expect(requireDetails(result).ok).toBe(false); @@ -299,8 +332,8 @@ describe("handleSlackAction", () => { slackConfig(), ); - expect(downloadSlackFile.mock.calls[0]?.[0]).toBe("F123"); - expectRecordFields(requireRecord(downloadSlackFile.mock.calls[0]?.[1], "download options"), { + expect(requireMockArg(downloadSlackFile, "downloadSlackFile", 0, 0)).toBe("F123"); + expectRecordFields(requireRecordArg(downloadSlackFile, "downloadSlackFile", 0, 1), { channelId: "C1", threadId: "123.456", }); @@ -343,8 +376,7 @@ describe("handleSlackAction", () => { it("forwards resolved botToken to action functions instead of relying on config re-read", async () => { downloadSlackFile.mockResolvedValueOnce(null); await handleSlackAction({ action: "downloadFile", fileId: "F123" }, slackConfig()); - const opts = downloadSlackFile.mock.calls[0]?.[1] as { token?: string } | undefined; - expect(opts?.token).toBe("tok"); + expect(requireRecordArg(downloadSlackFile, "downloadSlackFile", 0, 1).token).toBe("tok"); }); it("keeps resolved userToken for downloadFile reads when configured", async () => { @@ -360,8 +392,7 @@ describe("handleSlackAction", () => { }, }), ); - const opts = downloadSlackFile.mock.calls[0]?.[1] as { token?: string } | undefined; - expect(opts?.token).toBe("xoxp-user"); + expect(requireRecordArg(downloadSlackFile, "downloadSlackFile", 0, 1).token).toBe("xoxp-user"); }); it.each([ @@ -491,13 +522,17 @@ describe("handleSlackAction", () => { mediaUrl: "https://example.com/file.png", threadTs: undefined, }); - expect(sendSlackMessage.mock.calls[0]?.[2]).not.toHaveProperty("blocks"); + expect(requireRecordArg(sendSlackMessage, "sendSlackMessage", 0, 2)).not.toHaveProperty( + "blocks", + ); expectSlackSendCall(1, "channel:C123", "hello", { cfg, blocks: [{ type: "divider" }], threadTs: undefined, }); - expect(sendSlackMessage.mock.calls[1]?.[2]).not.toHaveProperty("mediaUrl"); + expect(requireRecordArg(sendSlackMessage, "sendSlackMessage", 1, 2)).not.toHaveProperty( + "mediaUrl", + ); expect(result.details).toEqual({ ok: true, result: { channelId: "C123" }, @@ -527,10 +562,11 @@ describe("handleSlackAction", () => { }, cfg, ); - expect(editSlackMessage.mock.calls[0]?.[0]).toBe("C123"); - expect(editSlackMessage.mock.calls[0]?.[1]).toBe("123.456"); - expect(editSlackMessage.mock.calls[0]?.[2]).toBe(""); - expectRecordFields(requireRecord(editSlackMessage.mock.calls[0]?.[3], "edit options"), { + const editCall = requireMockCall(editSlackMessage, "editSlackMessage"); + expect(editCall[0]).toBe("C123"); + expect(editCall[1]).toBe("123.456"); + expect(editCall[2]).toBe(""); + expectRecordFields(requireRecordArg(editSlackMessage, "editSlackMessage", 0, 3), { cfg, blocks: expectedBlocks, }); @@ -747,8 +783,8 @@ describe("handleSlackAction", () => { cfg, ); - expect(readSlackMessages.mock.calls[0]?.[0]).toBe("C1"); - expectRecordFields(requireRecord(readSlackMessages.mock.calls[0]?.[1], "read options"), { + expect(requireMockArg(readSlackMessages, "readSlackMessages", 0, 0)).toBe("C1"); + expectRecordFields(requireRecordArg(readSlackMessages, "readSlackMessages", 0, 1), { cfg, threadId: "1712345678.123456", limit: undefined, @@ -771,8 +807,8 @@ describe("handleSlackAction", () => { cfg, ); - expect(readSlackMessages.mock.calls[0]?.[0]).toBe("C1"); - expectRecordFields(requireRecord(readSlackMessages.mock.calls[0]?.[1], "read options"), { + expect(requireMockArg(readSlackMessages, "readSlackMessages", 0, 0)).toBe("C1"); + expectRecordFields(requireRecordArg(readSlackMessages, "readSlackMessages", 0, 1), { cfg, threadId: "1712345678.123456", messageId: "1712345678.654321",