From e8a1d4171d0f5cce11af5bd4dafb80b7c73befaf Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 16 Feb 2026 12:42:48 -0500 Subject: [PATCH] Slack: guard select option value length in slash menus --- src/slack/monitor/slash.test.ts | 64 ++++++++++++++++++++++++++++++--- src/slack/monitor/slash.ts | 6 +++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/slack/monitor/slash.test.ts b/src/slack/monitor/slash.test.ts index b172b1377..dec78e4f2 100644 --- a/src/slack/monitor/slash.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -4,6 +4,7 @@ import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.j vi.mock("../../auto-reply/commands-registry.js", () => { const usageCommand = { key: "usage", nativeName: "usage" }; const reportCommand = { key: "report", nativeName: "report" }; + const reportLongCommand = { key: "reportlong", nativeName: "reportlong" }; return { buildCommandTextFromArgs: ( @@ -30,6 +31,9 @@ vi.mock("../../auto-reply/commands-registry.js", () => { if (normalized === "report") { return reportCommand; } + if (normalized === "reportlong") { + return reportLongCommand; + } return undefined; }, listNativeCommandSpecsForConfig: () => [ @@ -45,16 +49,19 @@ vi.mock("../../auto-reply/commands-registry.js", () => { acceptsArgs: true, args: [], }, + { + name: "reportlong", + description: "ReportLong", + acceptsArgs: true, + args: [], + }, ], parseCommandArgs: () => ({ values: {} }), resolveCommandArgMenu: (params: { command?: { key?: string }; args?: { values?: unknown }; }) => { - if (params.command?.key !== "usage") { - if (params.command?.key !== "report") { - return null; - } + if (params.command?.key === "report") { const values = (params.args?.values ?? {}) as Record; if (typeof values.period === "string" && values.period.trim()) { return null; @@ -71,6 +78,23 @@ vi.mock("../../auto-reply/commands-registry.js", () => { ], }; } + if (params.command?.key === "reportlong") { + const values = (params.args?.values ?? {}) as Record; + if (typeof values.period === "string" && values.period.trim()) { + return null; + } + return { + arg: { name: "period", description: "period" }, + choices: [ + { value: "day", label: "day" }, + { value: "week", label: "week" }, + { value: "month", label: "month" }, + { value: "quarter", label: "quarter" }, + { value: "year", label: "year" }, + { value: "x".repeat(90), label: "long" }, + ], + }; + } if (params.command?.key !== "usage") { return null; } @@ -173,6 +197,7 @@ describe("Slack native command argument menus", () => { let harness: ReturnType; let usageHandler: (args: unknown) => Promise; let reportHandler: (args: unknown) => Promise; + let reportLongHandler: (args: unknown) => Promise; let argMenuHandler: (args: unknown) => Promise; beforeAll(async () => { @@ -189,6 +214,11 @@ describe("Slack native command argument menus", () => { throw new Error("Missing /report handler"); } reportHandler = report; + const reportLong = harness.commands.get("/reportlong"); + if (!reportLong) { + throw new Error("Missing /reportlong handler"); + } + reportLongHandler = reportLong; const argMenu = harness.actions.get("openclaw_cmdarg"); if (!argMenu) { @@ -255,6 +285,32 @@ describe("Slack native command argument menus", () => { expect(element?.action_id).toBe("openclaw_cmdarg"); }); + it("falls back to buttons when static_select value limit would be exceeded", async () => { + const respond = vi.fn().mockResolvedValue(undefined); + const ack = vi.fn().mockResolvedValue(undefined); + + await reportLongHandler({ + command: { + user_id: "U1", + user_name: "Ada", + channel_id: "C1", + channel_name: "directmessage", + text: "", + trigger_id: "t1", + }, + ack, + respond, + }); + + expect(respond).toHaveBeenCalledTimes(1); + const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; + expect(payload.blocks?.[1]?.type).toBe("actions"); + const firstElement = ( + payload.blocks?.[1] as { elements?: Array<{ type?: string }> } | undefined + )?.elements?.[0]; + expect(firstElement?.type).toBe("button"); + }); + it("dispatches the command when a menu button is clicked", async () => { const respond = vi.fn().mockResolvedValue(undefined); await argMenuHandler({ diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 9fa6b7f09..29c771418 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -31,6 +31,7 @@ const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg"; const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg"; const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5; const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100; +const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75; type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js"); let commandsRegistry: CommandsRegistry | undefined; @@ -111,8 +112,11 @@ function buildSlackCommandArgMenuBlocks(params: { userId: params.userId, }), })); + const canUseStaticSelect = encodedChoices.every( + (choice) => choice.value.length <= SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX, + ); const rows = - encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE + encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect ? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({ type: "actions", elements: choices.map((choice) => ({