From 3d921b61578e22847b9723c3e618d7c3adbb8b4d Mon Sep 17 00:00:00 2001 From: Marcus Castro Date: Fri, 13 Feb 2026 14:20:41 -0300 Subject: [PATCH] fix(slack): apply limit parameter to emoji-list action (#13421) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 67e9b648581c30a6472ac993dcc404e2d104ad1c Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete --- CHANGELOG.md | 1 + extensions/slack/src/channel.ts | 3 ++- src/agents/tools/slack-actions.e2e.test.ts | 22 ++++++++++++++++++ src/agents/tools/slack-actions.ts | 22 +++++++++++++++--- src/channels/plugins/slack.actions.test.ts | 26 +++++++++++++++++++++- src/channels/plugins/slack.actions.ts | 3 ++- 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 521cb84d5..cbba8a947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ Docs: https://docs.openclaw.ai - Telegram: surface REACTION_INVALID as non-fatal warning. (#14340) Thanks @0xRaini. - BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek. - Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de. +- Slack: honor `limit` for `emoji-list` actions across core and extension adapters, with capped emoji-list responses in the Slack action handler. (#4293) Thanks @mcaxtr. - Slack: detect control commands when channel messages start with bot mention prefixes (for example, `@Bot /new`). (#14142) Thanks @beefiker. - Slack: include thread reply metadata in inbound message footer context (`thread_ts`, `parent_user_id`) while keeping top-level `thread_ts == ts` events unthreaded. (#14625) Thanks @bennewton999. - Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins. diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index e55e43dcd..4b2586003 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -426,8 +426,9 @@ export const slackPlugin: ChannelPlugin = { } if (action === "emoji-list") { + const limit = readNumberParam(params, "limit", { integer: true }); return await getSlackRuntime().channel.slack.handleSlackAction( - { action: "emojiList", accountId: accountId ?? undefined }, + { action: "emojiList", limit, accountId: accountId ?? undefined }, cfg, ); } diff --git a/src/agents/tools/slack-actions.e2e.test.ts b/src/agents/tools/slack-actions.e2e.test.ts index 6ce3c8b95..94c518150 100644 --- a/src/agents/tools/slack-actions.e2e.test.ts +++ b/src/agents/tools/slack-actions.e2e.test.ts @@ -432,4 +432,26 @@ describe("handleSlackAction", () => { const [, , opts] = sendSlackMessage.mock.calls[0] ?? []; expect(opts?.token).toBe("xoxp-1"); }); + + it("returns all emojis when no limit is provided", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const emojiMap = { wave: "url1", smile: "url2", heart: "url3" }; + listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap }); + const result = await handleSlackAction({ action: "emojiList" }, cfg); + const payload = result.details as { ok: boolean; emojis: { emoji: Record } }; + expect(payload.ok).toBe(true); + expect(Object.keys(payload.emojis.emoji)).toHaveLength(3); + }); + + it("applies limit to emoji-list results", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const emojiMap = { wave: "url1", smile: "url2", heart: "url3", fire: "url4", star: "url5" }; + listSlackEmojis.mockResolvedValueOnce({ ok: true, emoji: emojiMap }); + const result = await handleSlackAction({ action: "emojiList", limit: 2 }, cfg); + const payload = result.details as { ok: boolean; emojis: { emoji: Record } }; + expect(payload.ok).toBe(true); + const emojiKeys = Object.keys(payload.emojis.emoji); + expect(emojiKeys).toHaveLength(2); + expect(emojiKeys.every((k) => k in emojiMap)).toBe(true); + }); }); diff --git a/src/agents/tools/slack-actions.ts b/src/agents/tools/slack-actions.ts index e4de2472a..97198e3fe 100644 --- a/src/agents/tools/slack-actions.ts +++ b/src/agents/tools/slack-actions.ts @@ -18,7 +18,13 @@ import { } from "../../slack/actions.js"; import { parseSlackTarget, resolveSlackChannelId } from "../../slack/targets.js"; import { withNormalizedTimestamp } from "../date-time.js"; -import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js"; +import { + createActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringParam, +} from "./common.js"; const messagingActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]); @@ -305,8 +311,18 @@ export async function handleSlackAction( if (!isActionEnabled("emojiList")) { throw new Error("Slack emoji list is disabled."); } - const emojis = readOpts ? await listSlackEmojis(readOpts) : await listSlackEmojis(); - return jsonResult({ ok: true, emojis }); + const result = readOpts ? await listSlackEmojis(readOpts) : await listSlackEmojis(); + const limit = readNumberParam(params, "limit", { integer: true }); + if (limit != null && limit > 0 && result.emoji != null) { + const entries = Object.entries(result.emoji).toSorted(([a], [b]) => a.localeCompare(b)); + if (entries.length > limit) { + return jsonResult({ + ok: true, + emojis: { ...result, emoji: Object.fromEntries(entries.slice(0, limit)) }, + }); + } + } + return jsonResult({ ok: true, emojis: result }); } throw new Error(`Unknown action: ${action}`); diff --git a/src/channels/plugins/slack.actions.test.ts b/src/channels/plugins/slack.actions.test.ts index a6644e396..844da4f09 100644 --- a/src/channels/plugins/slack.actions.test.ts +++ b/src/channels/plugins/slack.actions.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 { createSlackActions } from "./slack.actions.js"; @@ -9,6 +9,10 @@ vi.mock("../../agents/tools/slack-actions.js", () => ({ })); describe("slack actions adapter", () => { + beforeEach(() => { + handleSlackAction.mockClear(); + }); + it("forwards threadId for read", async () => { const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; const actions = createSlackActions("slack"); @@ -30,4 +34,24 @@ describe("slack actions adapter", () => { threadId: "171234.567", }); }); + + it("forwards normalized limit for emoji-list", async () => { + const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig; + const actions = createSlackActions("slack"); + + await actions.handleAction?.({ + channel: "slack", + action: "emoji-list", + cfg, + params: { + limit: "2.9", + }, + }); + + const [params] = handleSlackAction.mock.calls[0] ?? []; + expect(params).toMatchObject({ + action: "emojiList", + limit: 2, + }); + }); }); diff --git a/src/channels/plugins/slack.actions.ts b/src/channels/plugins/slack.actions.ts index 60601f4fd..81eaa92b7 100644 --- a/src/channels/plugins/slack.actions.ts +++ b/src/channels/plugins/slack.actions.ts @@ -210,8 +210,9 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap } if (action === "emoji-list") { + const limit = readNumberParam(params, "limit", { integer: true }); return await handleSlackAction( - { action: "emojiList", accountId: accountId ?? undefined }, + { action: "emojiList", limit, accountId: accountId ?? undefined }, cfg, ); }