fix: cap slack command menu blocks

This commit is contained in:
Peter Steinberger
2026-04-30 03:57:36 +01:00
parent a4af1e91da
commit d25cfda54c
3 changed files with 43 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ vi.mock("./slash-commands.runtime.js", () => {
const reportExternalCommand = { key: "reportexternal", nativeName: "reportexternal" };
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
const reportLongButtonCommand = { key: "reportlongbutton", nativeName: "reportlongbutton" };
const reportHugeButtonCommand = { key: "reporthugebutton", nativeName: "reporthugebutton" };
const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" };
const longConfirmCommand = { key: "longconfirm", nativeName: "longconfirm" };
const statusAliasCommand = { key: "status", nativeName: "status" };
@@ -76,6 +77,9 @@ vi.mock("./slash-commands.runtime.js", () => {
if (normalized === "reportlongbutton") {
return reportLongButtonCommand;
}
if (normalized === "reporthugebutton") {
return reportHugeButtonCommand;
}
if (normalized === "unsafeconfirm") {
return unsafeConfirmCommand;
}
@@ -124,6 +128,12 @@ vi.mock("./slash-commands.runtime.js", () => {
acceptsArgs: true,
args: [],
},
{
name: "reporthugebutton",
description: "ReportHugeButton",
acceptsArgs: true,
args: [],
},
{
name: "unsafeconfirm",
description: "UnsafeConfirm",
@@ -168,6 +178,15 @@ vi.mock("./slash-commands.runtime.js", () => {
},
]);
}
if (params.command?.key === "reporthugebutton") {
return resolvePeriodMenu(
params,
Array.from({ length: 250 }, (_v, i) => ({
value: `${String(i + 1)}-${"x".repeat(170)}`,
label: `Long button label ${i + 1}`,
})),
);
}
if (params.command?.key === "reportcompact") {
return resolvePeriodMenu(params, baseReportPeriodChoices);
}
@@ -446,6 +465,7 @@ describe("Slack native command argument menus", () => {
let reportExternalHandler: (args: unknown) => Promise<void>;
let reportLongHandler: (args: unknown) => Promise<void>;
let reportLongButtonHandler: (args: unknown) => Promise<void>;
let reportHugeButtonHandler: (args: unknown) => Promise<void>;
let unsafeConfirmHandler: (args: unknown) => Promise<void>;
let longConfirmHandler: (args: unknown) => Promise<void>;
let agentStatusHandler: (args: unknown) => Promise<void>;
@@ -465,6 +485,11 @@ describe("Slack native command argument menus", () => {
"/reportlongbutton",
"/reportlongbutton",
);
reportHugeButtonHandler = requireHandler(
harness.commands,
"/reporthugebutton",
"/reporthugebutton",
);
unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm");
longConfirmHandler = requireHandler(harness.commands, "/longconfirm", "/longconfirm");
agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus");
@@ -601,6 +626,18 @@ describe("Slack native command argument menus", () => {
expect(firstElement?.confirm).toBeTruthy();
});
it("caps large button fallback menus to Slack's block limit", async () => {
const { respond } = await runCommandHandler(reportHugeButtonHandler);
expect(respond).toHaveBeenCalledTimes(1);
const payload = respond.mock.calls[0]?.[0] as {
blocks?: Array<{ type: string; elements?: unknown[] }>;
};
const actionBlocks = (payload.blocks ?? []).filter((block) => block.type === "actions");
expect(payload.blocks).toHaveLength(50);
expect(actionBlocks).toHaveLength(47);
expect(actionBlocks.at(-1)?.elements).toHaveLength(5);
});
it("shows an overflow menu when choices fit compact range", async () => {
const element = await getFirstActionElementFromCommand(reportCompactHandler);
expect(element?.type).toBe("overflow");

View File

@@ -26,6 +26,7 @@ import {
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type { ResolvedSlackAccount } from "../accounts.js";
import { SLACK_MAX_BLOCKS } from "../blocks-input.js";
import { truncateSlackText } from "../truncate.js";
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "./allow-list.js";
import { resolveSlackEffectiveAllowFrom } from "./auth.js";
@@ -57,6 +58,8 @@ const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75;
const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
const SLACK_HEADER_TEXT_MAX = 150;
const SLACK_COMMAND_ARG_CHROME_BLOCKS = 3;
const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = SLACK_MAX_BLOCKS - SLACK_COMMAND_ARG_CHROME_BLOCKS;
let slashCommandsRuntimePromise: Promise<typeof import("./slash-commands.runtime.js")> | null =
null;
let slashDispatchRuntimePromise: Promise<typeof import("./slash-dispatch.runtime.js")> | null =
@@ -338,6 +341,7 @@ function buildSlackCommandArgMenuBlocks(params: {
`Select one option to continue /${params.command} (${params.arg})`,
3000,
);
const visibleRows = rows.slice(0, SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX);
return [
{
type: "header",
@@ -351,7 +355,7 @@ function buildSlackCommandArgMenuBlocks(params: {
type: "context",
elements: [{ type: "mrkdwn", text: contextText }],
},
...rows,
...visibleRows,
];
}