From 22d979f0abcbdfab2de19fb659db20ea229ad52f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 18:26:47 +0100 Subject: [PATCH] test: clear slack message action broad matchers --- .../slack/src/message-action-dispatch.test.ts | 240 ++++++++---------- 1 file changed, 106 insertions(+), 134 deletions(-) diff --git a/extensions/slack/src/message-action-dispatch.test.ts b/extensions/slack/src/message-action-dispatch.test.ts index 6fe3df2dec6..81909977d11 100644 --- a/extensions/slack/src/message-action-dispatch.test.ts +++ b/extensions/slack/src/message-action-dispatch.test.ts @@ -16,6 +16,32 @@ function expectForwardedCfg(invoke: ReturnType, cfg: unk expect(invoke.mock.calls[0]?.[1]).toBe(cfg); } +function firstAction(invoke: ReturnType) { + const action = invoke.mock.calls[0]?.[0]; + if (!action || typeof action !== "object") { + throw new Error("expected first invoke action"); + } + return action; +} + +function blockAt(action: Record, index: number) { + const blocks = action.blocks as Array> | undefined; + const block = blocks?.[index]; + if (!block) { + throw new Error(`expected Slack block ${index}`); + } + return block; +} + +function elementAt(block: Record, index: number) { + const elements = block.elements as Array> | undefined; + const element = elements?.[index]; + if (!element) { + throw new Error(`expected Slack block element ${index}`); + } + return element; +} + describe("handleSlackMessageAction", () => { it("merges presentation and interactive blocks when sending", async () => { const invoke = createInvokeSpy(); @@ -44,16 +70,11 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - const action = invoke.mock.calls[0]?.[0] as { - blocks?: Array<{ type?: string; elements?: Array<{ value?: string }> }>; - }; - expect(action.blocks).toEqual([ - expect.objectContaining({ type: "section" }), - expect.objectContaining({ - type: "actions", - elements: [expect.objectContaining({ value: "approve" })], - }), - ]); + const action = firstAction(invoke); + expect(blockAt(action, 0).type).toBe("section"); + const actionsBlock = blockAt(action, 1); + expect(actionsBlock.type).toBe("actions"); + expect(elementAt(actionsBlock, 0).value).toBe("approve"); }); it("keeps generated Slack control ids unique when presentation and interactive controls are merged", async () => { @@ -88,23 +109,13 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - const action = invoke.mock.calls[0]?.[0] as { - blocks?: Array<{ - block_id?: string; - elements?: Array<{ action_id?: string; value?: string }>; - }>; - }; - - expect(action.blocks).toEqual([ - expect.objectContaining({ - block_id: "openclaw_reply_buttons_1", - elements: [expect.objectContaining({ action_id: "openclaw:reply_button:1:1" })], - }), - expect.objectContaining({ - block_id: "openclaw_reply_buttons_2", - elements: [expect.objectContaining({ action_id: "openclaw:reply_button:2:1" })], - }), - ]); + const action = firstAction(invoke); + const firstButtons = blockAt(action, 0); + expect(firstButtons.block_id).toBe("openclaw_reply_buttons_1"); + expect(elementAt(firstButtons, 0).action_id).toBe("openclaw:reply_button:1:1"); + const secondButtons = blockAt(action, 1); + expect(secondButtons.block_id).toBe("openclaw_reply_buttons_2"); + expect(elementAt(secondButtons, 0).action_id).toBe("openclaw:reply_button:2:1"); }); it("passes media and rendered interactive blocks through for split Slack delivery", async () => { @@ -134,23 +145,16 @@ describe("handleSlackMessageAction", () => { }); expect(invoke).toHaveBeenCalledOnce(); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "sendMessage", - to: "channel:C1", - content: "Approval required", - mediaUrl: "https://example.com/report.md", - blocks: [ - expect.objectContaining({ - type: "actions", - elements: [expect.objectContaining({ value: "approve" })], - }), - ], - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("sendMessage"); + expect(action.to).toBe("channel:C1"); + expect(action.content).toBe("Approval required"); + expect(action.mediaUrl).toBe("https://example.com/report.md"); + const actionsBlock = blockAt(action, 0); + expect(actionsBlock.type).toBe("actions"); + expect(elementAt(actionsBlock, 0).value).toBe("approve"); expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("passes replyBroadcast through for Slack thread sends", async () => { @@ -172,17 +176,14 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "sendMessage", - to: "channel:C1", - content: "Visible from the channel", - threadTs: "111.222", - replyBroadcast: true, - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("sendMessage"); + expect(action.to).toBe("channel:C1"); + expect(action.content).toBe("Visible from the channel"); + expect(action.threadTs).toBe("111.222"); + expect(action.replyBroadcast).toBe(true); + expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("passes topLevel through so same-channel Slack sends can suppress thread inheritance", async () => { @@ -203,17 +204,14 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "sendMessage", - to: "channel:C1", - content: "Visible in the parent channel", - threadTs: undefined, - topLevel: true, - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("sendMessage"); + expect(action.to).toBe("channel:C1"); + expect(action.content).toBe("Visible in the parent channel"); + expect(action.threadTs).toBeUndefined(); + expect(action.topLevel).toBe(true); + expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("treats threadId null as a Slack top-level send request", async () => { @@ -234,15 +232,12 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "sendMessage", - threadTs: undefined, - topLevel: true, - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("sendMessage"); + expect(action.threadTs).toBeUndefined(); + expect(action.topLevel).toBe(true); + expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("maps upload-file to the internal uploadFile action", async () => { @@ -266,20 +261,16 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "uploadFile", - to: "user:U1", - filePath: "/tmp/report.png", - initialComment: "fresh build", - filename: "build.png", - title: "Build Screenshot", - threadTs: "111.222", - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("uploadFile"); + expect(action.to).toBe("user:U1"); + expect(action.filePath).toBe("/tmp/report.png"); + expect(action.initialComment).toBe("fresh build"); + expect(action.filename).toBe("build.png"); + expect(action.title).toBe("Build Screenshot"); + expect(action.threadTs).toBe("111.222"); expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("rejects replyBroadcast for upload-file", async () => { @@ -320,18 +311,14 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "uploadFile", - to: "C1", - filePath: "/tmp/chart.png", - initialComment: "chart attached", - threadTs: "333.444", - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("uploadFile"); + expect(action.to).toBe("C1"); + expect(action.filePath).toBe("/tmp/chart.png"); + expect(action.initialComment).toBe("chart attached"); + expect(action.threadTs).toBe("333.444"); expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("maps upload-file path alias to filePath", async () => { @@ -352,17 +339,13 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "uploadFile", - to: "channel:C1", - filePath: "/tmp/report.txt", - initialComment: "path alias", - }), - cfg, - undefined, - ); + const action = firstAction(invoke); + expect(action.action).toBe("uploadFile"); + expect(action.to).toBe("channel:C1"); + expect(action.filePath).toBe("/tmp/report.txt"); + expect(action.initialComment).toBe("path alias"); expectForwardedCfg(invoke, cfg); + expect(invoke.mock.calls[0]?.[2]).toBeUndefined(); }); it("forwards messageId for read actions", async () => { @@ -381,14 +364,11 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "readMessages", - channelId: "C1", - messageId: "1712345678.654321", - }), - {}, - ); + const action = firstAction(invoke); + expect(action.action).toBe("readMessages"); + expect(action.channelId).toBe("C1"); + expect(action.messageId).toBe("1712345678.654321"); + expect(invoke.mock.calls[0]?.[1]).toEqual({}); }); it("requires filePath, path, or media for upload-file", async () => { @@ -425,15 +405,11 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "downloadFile", - fileId: "F123", - channelId: "C1", - threadId: "111.222", - }), - cfg, - ); + const action = firstAction(invoke); + expect(action.action).toBe("downloadFile"); + expect(action.fileId).toBe("F123"); + expect(action.channelId).toBe("C1"); + expect(action.threadId).toBe("111.222"); expectForwardedCfg(invoke, cfg); }); @@ -455,15 +431,11 @@ describe("handleSlackMessageAction", () => { invoke: invoke as never, }); - expect(invoke).toHaveBeenCalledWith( - expect.objectContaining({ - action: "downloadFile", - fileId: "F999", - channelId: "channel:C2", - threadId: "333.444", - }), - cfg, - ); + const action = firstAction(invoke); + expect(action.action).toBe("downloadFile"); + expect(action.fileId).toBe("F999"); + expect(action.channelId).toBe("channel:C2"); + expect(action.threadId).toBe("333.444"); expectForwardedCfg(invoke, cfg); });