From 2595690a4d0372e29e920ba66208873b9c998735 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 23:41:40 +0000 Subject: [PATCH] test(actions): table-drive slack and telegram action cases --- src/agents/tools/slack-actions.e2e.test.ts | 233 ++++++++---------- src/agents/tools/telegram-actions.e2e.test.ts | 171 ++++++------- 2 files changed, 173 insertions(+), 231 deletions(-) diff --git a/src/agents/tools/slack-actions.e2e.test.ts b/src/agents/tools/slack-actions.e2e.test.ts index 7c3d6effb6e..fffeb528a13 100644 --- a/src/agents/tools/slack-actions.e2e.test.ts +++ b/src/agents/tools/slack-actions.e2e.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { handleSlackAction } from "./slack-actions.js"; @@ -17,52 +17,59 @@ const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({})); vi.mock("../../slack/actions.js", () => ({ - deleteSlackMessage, - editSlackMessage, - getSlackMemberInfo, - listSlackEmojis, - listSlackPins, - listSlackReactions, - pinSlackMessage, - reactSlackMessage, - readSlackMessages, - removeOwnSlackReactions, - removeSlackReaction, - sendSlackMessage, - unpinSlackMessage, + deleteSlackMessage: (...args: Parameters) => + deleteSlackMessage(...args), + editSlackMessage: (...args: Parameters) => editSlackMessage(...args), + getSlackMemberInfo: (...args: Parameters) => + getSlackMemberInfo(...args), + listSlackEmojis: (...args: Parameters) => listSlackEmojis(...args), + listSlackPins: (...args: Parameters) => listSlackPins(...args), + listSlackReactions: (...args: Parameters) => + listSlackReactions(...args), + pinSlackMessage: (...args: Parameters) => pinSlackMessage(...args), + reactSlackMessage: (...args: Parameters) => reactSlackMessage(...args), + readSlackMessages: (...args: Parameters) => readSlackMessages(...args), + removeOwnSlackReactions: (...args: Parameters) => + removeOwnSlackReactions(...args), + removeSlackReaction: (...args: Parameters) => + removeSlackReaction(...args), + sendSlackMessage: (...args: Parameters) => sendSlackMessage(...args), + unpinSlackMessage: (...args: Parameters) => unpinSlackMessage(...args), })); describe("handleSlackAction", () => { - it("adds reactions", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - await handleSlackAction( - { - action: "react", - channelId: "C1", - messageId: "123.456", - emoji: "✅", + function slackConfig(overrides?: Record): OpenClawConfig { + return { + channels: { + slack: { + botToken: "tok", + ...overrides, + }, }, - cfg, - ); - expect(reactSlackMessage).toHaveBeenCalledWith("C1", "123.456", "✅"); + } as OpenClawConfig; + } + + beforeEach(() => { + vi.clearAllMocks(); }); - it("strips channel: prefix for channelId params", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + it.each([ + { name: "raw channel id", channelId: "C1" }, + { name: "channel: prefixed id", channelId: "channel:C1" }, + ])("adds reactions for $name", async ({ channelId }) => { await handleSlackAction( { action: "react", - channelId: "channel:C1", + channelId, messageId: "123.456", emoji: "✅", }, - cfg, + slackConfig(), ); expect(reactSlackMessage).toHaveBeenCalledWith("C1", "123.456", "✅"); }); it("removes reactions on empty emoji", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await handleSlackAction( { action: "react", @@ -70,13 +77,12 @@ describe("handleSlackAction", () => { messageId: "123.456", emoji: "", }, - cfg, + slackConfig(), ); expect(removeOwnSlackReactions).toHaveBeenCalledWith("C1", "123.456"); }); it("removes reactions when remove flag set", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await handleSlackAction( { action: "react", @@ -85,13 +91,12 @@ describe("handleSlackAction", () => { emoji: "✅", remove: true, }, - cfg, + slackConfig(), ); expect(removeSlackReaction).toHaveBeenCalledWith("C1", "123.456", "✅"); }); it("rejects removes without emoji", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await expect( handleSlackAction( { @@ -101,15 +106,12 @@ describe("handleSlackAction", () => { emoji: "", remove: true, }, - cfg, + slackConfig(), ), ).rejects.toThrow(/Emoji is required/); }); it("respects reaction gating", async () => { - const cfg = { - channels: { slack: { botToken: "tok", actions: { reactions: false } } }, - } as OpenClawConfig; await expect( handleSlackAction( { @@ -118,13 +120,12 @@ describe("handleSlackAction", () => { messageId: "123.456", emoji: "✅", }, - cfg, + slackConfig({ actions: { reactions: false } }), ), ).rejects.toThrow(/Slack reactions are disabled/); }); it("passes threadTs to sendSlackMessage for thread replies", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await handleSlackAction( { action: "sendMessage", @@ -132,7 +133,7 @@ describe("handleSlackAction", () => { content: "Hello thread", threadTs: "1234567890.123456", }, - cfg, + slackConfig(), ); expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Hello thread", { mediaUrl: undefined, @@ -141,74 +142,56 @@ describe("handleSlackAction", () => { }); }); - it("accepts blocks JSON and allows empty content", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); - await handleSlackAction( - { - action: "sendMessage", - to: "channel:C123", - blocks: JSON.stringify([ - { type: "section", text: { type: "mrkdwn", text: "*Deploy* status" } }, - ]), - }, - cfg, - ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "", { - mediaUrl: undefined, - threadTs: undefined, - blocks: [{ type: "section", text: { type: "mrkdwn", text: "*Deploy* status" } }], - }); - }); - - it("accepts blocks arrays directly", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - sendSlackMessage.mockClear(); - await handleSlackAction( - { - action: "sendMessage", - to: "channel:C123", - blocks: [{ type: "divider" }], - }, - cfg, - ); - expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "", { - mediaUrl: undefined, - threadTs: undefined, + it.each([ + { + name: "JSON blocks", + blocks: JSON.stringify([ + { type: "section", text: { type: "mrkdwn", text: "*Deploy* status" } }, + ]), + expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "*Deploy* status" } }], + }, + { + name: "array blocks", blocks: [{ type: "divider" }], + expectedBlocks: [{ type: "divider" }], + }, + ])("accepts $name and allows empty content", async ({ blocks, expectedBlocks }) => { + await handleSlackAction( + { + action: "sendMessage", + to: "channel:C123", + blocks, + }, + slackConfig(), + ); + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "", { + mediaUrl: undefined, + threadTs: undefined, + blocks: expectedBlocks, }); }); - it("rejects invalid blocks JSON", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + it.each([ + { + name: "invalid blocks JSON", + blocks: "{bad-json", + expectedError: /blocks must be valid JSON/i, + }, + { name: "empty blocks arrays", blocks: "[]", expectedError: /at least one block/i }, + ])("rejects $name", async ({ blocks, expectedError }) => { await expect( handleSlackAction( { action: "sendMessage", to: "channel:C123", - blocks: "{bad-json", + blocks, }, - cfg, + slackConfig(), ), - ).rejects.toThrow(/blocks must be valid JSON/i); - }); - - it("rejects empty blocks arrays", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - await expect( - handleSlackAction( - { - action: "sendMessage", - to: "channel:C123", - blocks: "[]", - }, - cfg, - ), - ).rejects.toThrow(/at least one block/i); + ).rejects.toThrow(expectedError); }); it("requires at least one of content, blocks, or mediaUrl", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await expect( handleSlackAction( { @@ -216,13 +199,12 @@ describe("handleSlackAction", () => { to: "channel:C123", content: "", }, - cfg, + slackConfig(), ), ).rejects.toThrow(/requires content, blocks, or mediaUrl/i); }); it("rejects blocks combined with mediaUrl", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await expect( handleSlackAction( { @@ -231,47 +213,38 @@ describe("handleSlackAction", () => { blocks: [{ type: "divider" }], mediaUrl: "https://example.com/image.png", }, - cfg, + slackConfig(), ), ).rejects.toThrow(/does not support blocks with mediaUrl/i); }); - it("passes blocks JSON to editSlackMessage with empty content", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - editSlackMessage.mockClear(); - await handleSlackAction( - { - action: "editMessage", - channelId: "C123", - messageId: "123.456", - blocks: JSON.stringify([{ type: "section", text: { type: "mrkdwn", text: "Updated" } }]), - }, - cfg, - ); - expect(editSlackMessage).toHaveBeenCalledWith("C123", "123.456", "", { - blocks: [{ type: "section", text: { type: "mrkdwn", text: "Updated" } }], - }); - }); - - it("passes blocks arrays to editSlackMessage", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; - editSlackMessage.mockClear(); - await handleSlackAction( - { - action: "editMessage", - channelId: "C123", - messageId: "123.456", - blocks: [{ type: "divider" }], - }, - cfg, - ); - expect(editSlackMessage).toHaveBeenCalledWith("C123", "123.456", "", { + it.each([ + { + name: "JSON blocks", + blocks: JSON.stringify([{ type: "section", text: { type: "mrkdwn", text: "Updated" } }]), + expectedBlocks: [{ type: "section", text: { type: "mrkdwn", text: "Updated" } }], + }, + { + name: "array blocks", blocks: [{ type: "divider" }], + expectedBlocks: [{ type: "divider" }], + }, + ])("passes $name to editSlackMessage", async ({ blocks, expectedBlocks }) => { + await handleSlackAction( + { + action: "editMessage", + channelId: "C123", + messageId: "123.456", + blocks, + }, + slackConfig(), + ); + expect(editSlackMessage).toHaveBeenCalledWith("C123", "123.456", "", { + blocks: expectedBlocks, }); }); it("requires content or blocks for editMessage", async () => { - const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; await expect( handleSlackAction( { @@ -280,7 +253,7 @@ describe("handleSlackAction", () => { messageId: "123.456", content: "", }, - cfg, + slackConfig(), ), ).rejects.toThrow(/requires content or blocks/i); }); diff --git a/src/agents/tools/telegram-actions.e2e.test.ts b/src/agents/tools/telegram-actions.e2e.test.ts index 42d2b9d2f7d..395f29a59f3 100644 --- a/src/agents/tools/telegram-actions.e2e.test.ts +++ b/src/agents/tools/telegram-actions.e2e.test.ts @@ -40,6 +40,17 @@ describe("handleTelegramAction", () => { } as OpenClawConfig; } + function telegramConfig(overrides?: Record): OpenClawConfig { + return { + channels: { + telegram: { + botToken: "tok", + ...overrides, + }, + }, + } as OpenClawConfig; + } + async function expectReactionAdded(reactionLevel: "minimal" | "extensive") { await handleTelegramAction(defaultReactionAction, reactionConfig(reactionLevel)); expect(reactMessageTelegram).toHaveBeenCalledWith( @@ -166,8 +177,16 @@ describe("handleTelegramAction", () => { ); }); - it("blocks reactions when reactionLevel is off", async () => { - const cfg = reactionConfig("off"); + it.each([ + { + level: "off" as const, + expectedMessage: /Telegram agent reactions disabled.*reactionLevel="off"/, + }, + { + level: "ack" as const, + expectedMessage: /Telegram agent reactions disabled.*reactionLevel="ack"/, + }, + ])("blocks reactions when reactionLevel is $level", async ({ level, expectedMessage }) => { await expect( handleTelegramAction( { @@ -176,24 +195,9 @@ describe("handleTelegramAction", () => { messageId: "456", emoji: "✅", }, - cfg, + reactionConfig(level), ), - ).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="off"/); - }); - - it("blocks reactions when reactionLevel is ack", async () => { - const cfg = reactionConfig("ack"); - await expect( - handleTelegramAction( - { - action: "react", - chatId: "123", - messageId: "456", - emoji: "✅", - }, - cfg, - ), - ).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="ack"/); + ).rejects.toThrow(expectedMessage); }); it("also respects legacy actions.reactions gating", async () => { @@ -220,16 +224,13 @@ describe("handleTelegramAction", () => { }); it("sends a text message", async () => { - const cfg = { - channels: { telegram: { botToken: "tok" } }, - } as OpenClawConfig; const result = await handleTelegramAction( { action: "sendMessage", to: "@testchannel", content: "Hello, Telegram!", }, - cfg, + telegramConfig(), ); expect(sendMessageTelegram).toHaveBeenCalledWith( "@testchannel", @@ -242,87 +243,66 @@ describe("handleTelegramAction", () => { }); }); - it("sends a message with media", async () => { - const cfg = { - channels: { telegram: { botToken: "tok" } }, - } as OpenClawConfig; - await handleTelegramAction( - { + it.each([ + { + name: "media", + params: { action: "sendMessage", to: "123456", content: "Check this image!", mediaUrl: "https://example.com/image.jpg", }, - cfg, - ); - expect(sendMessageTelegram).toHaveBeenCalledWith( - "123456", - "Check this image!", - expect.objectContaining({ - token: "tok", - mediaUrl: "https://example.com/image.jpg", - }), - ); - }); - - it("passes quoteText when provided", async () => { - const cfg = { - channels: { telegram: { botToken: "tok" } }, - } as OpenClawConfig; - await handleTelegramAction( - { + expectedTo: "123456", + expectedContent: "Check this image!", + expectedOptions: { mediaUrl: "https://example.com/image.jpg" }, + }, + { + name: "quoteText", + params: { action: "sendMessage", to: "123456", content: "Replying now", replyToMessageId: 144, quoteText: "The text you want to quote", }, - cfg, - ); - expect(sendMessageTelegram).toHaveBeenCalledWith( - "123456", - "Replying now", - expect.objectContaining({ - token: "tok", + expectedTo: "123456", + expectedContent: "Replying now", + expectedOptions: { replyToMessageId: 144, quoteText: "The text you want to quote", - }), - ); - }); - - it("allows media-only messages without content", async () => { - const cfg = { - channels: { telegram: { botToken: "tok" } }, - } as OpenClawConfig; - await handleTelegramAction( - { + }, + }, + { + name: "media-only", + params: { action: "sendMessage", to: "123456", mediaUrl: "https://example.com/note.ogg", }, - cfg, - ); + expectedTo: "123456", + expectedContent: "", + expectedOptions: { mediaUrl: "https://example.com/note.ogg" }, + }, + ] as const)("maps sendMessage params for $name", async (testCase) => { + await handleTelegramAction(testCase.params, telegramConfig()); expect(sendMessageTelegram).toHaveBeenCalledWith( - "123456", - "", + testCase.expectedTo, + testCase.expectedContent, expect.objectContaining({ token: "tok", - mediaUrl: "https://example.com/note.ogg", + ...testCase.expectedOptions, }), ); }); it("requires content when no mediaUrl is provided", async () => { - const cfg = { - channels: { telegram: { botToken: "tok" } }, - } as OpenClawConfig; await expect( handleTelegramAction( { action: "sendMessage", to: "123456", }, - cfg, + telegramConfig(), ), ).rejects.toThrow(/content required/i); }); @@ -413,42 +393,31 @@ describe("handleTelegramAction", () => { expect(sendMessageTelegram).toHaveBeenCalled(); }); - it("blocks inline buttons when scope is off", async () => { - const cfg = { - channels: { - telegram: { botToken: "tok", capabilities: { inlineButtons: "off" } }, - }, - } as OpenClawConfig; + it.each([ + { + name: "scope is off", + to: "@testchannel", + inlineButtons: "off" as const, + expectedMessage: /inline buttons are disabled/i, + }, + { + name: "scope is dm and target is group", + to: "-100123456", + inlineButtons: "dm" as const, + expectedMessage: /inline buttons are limited to DMs/i, + }, + ])("blocks inline buttons when $name", async ({ to, inlineButtons, expectedMessage }) => { await expect( handleTelegramAction( { action: "sendMessage", - to: "@testchannel", + to, content: "Choose", buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]], }, - cfg, + telegramConfig({ capabilities: { inlineButtons } }), ), - ).rejects.toThrow(/inline buttons are disabled/i); - }); - - it("blocks inline buttons in groups when scope is dm", async () => { - const cfg = { - channels: { - telegram: { botToken: "tok", capabilities: { inlineButtons: "dm" } }, - }, - } as OpenClawConfig; - await expect( - handleTelegramAction( - { - action: "sendMessage", - to: "-100123456", - content: "Choose", - buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]], - }, - cfg, - ), - ).rejects.toThrow(/inline buttons are limited to DMs/i); + ).rejects.toThrow(expectedMessage); }); it("allows inline buttons in DMs with tg: prefixed targets", async () => {