fix: harden slack command menus

This commit is contained in:
Peter Steinberger
2026-04-30 03:03:49 +01:00
parent 1f006dbc5f
commit 1a103088ba
3 changed files with 48 additions and 4 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.
- Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose `speechReady` diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf.
- Slack/commands: keep native command argument menus on select controls for encoded choice values up to Slack's option limit and truncate fallback button labels to Slack's button-text limit, so long valid choices no longer render invalid Slack blocks. Thanks @slackapi.
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
- Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.

View File

@@ -7,6 +7,7 @@ vi.mock("./slash-commands.runtime.js", () => {
const reportCompactCommand = { key: "reportcompact", nativeName: "reportcompact" };
const reportExternalCommand = { key: "reportexternal", nativeName: "reportexternal" };
const reportLongCommand = { key: "reportlong", nativeName: "reportlong" };
const reportLongButtonCommand = { key: "reportlongbutton", nativeName: "reportlongbutton" };
const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" };
const statusAliasCommand = { key: "status", nativeName: "status" };
const periodArg = { name: "period", description: "period" };
@@ -71,6 +72,9 @@ vi.mock("./slash-commands.runtime.js", () => {
if (normalized === "reportlong") {
return reportLongCommand;
}
if (normalized === "reportlongbutton") {
return reportLongButtonCommand;
}
if (normalized === "unsafeconfirm") {
return unsafeConfirmCommand;
}
@@ -110,6 +114,12 @@ vi.mock("./slash-commands.runtime.js", () => {
acceptsArgs: true,
args: [],
},
{
name: "reportlongbutton",
description: "ReportLongButton",
acceptsArgs: true,
args: [],
},
{
name: "unsafeconfirm",
description: "UnsafeConfirm",
@@ -140,6 +150,14 @@ vi.mock("./slash-commands.runtime.js", () => {
{ value: "x".repeat(90), label: "long" },
]);
}
if (params.command?.key === "reportlongbutton") {
return resolvePeriodMenu(params, [
{
value: "x".repeat(170),
label: "Long button label ".repeat(8),
},
]);
}
if (params.command?.key === "reportcompact") {
return resolvePeriodMenu(params, baseReportPeriodChoices);
}
@@ -408,6 +426,7 @@ describe("Slack native command argument menus", () => {
let reportCompactHandler: (args: unknown) => Promise<void>;
let reportExternalHandler: (args: unknown) => Promise<void>;
let reportLongHandler: (args: unknown) => Promise<void>;
let reportLongButtonHandler: (args: unknown) => Promise<void>;
let unsafeConfirmHandler: (args: unknown) => Promise<void>;
let agentStatusHandler: (args: unknown) => Promise<void>;
let argMenuHandler: (args: unknown) => Promise<void>;
@@ -421,6 +440,11 @@ describe("Slack native command argument menus", () => {
reportCompactHandler = requireHandler(harness.commands, "/reportcompact", "/reportcompact");
reportExternalHandler = requireHandler(harness.commands, "/reportexternal", "/reportexternal");
reportLongHandler = requireHandler(harness.commands, "/reportlong", "/reportlong");
reportLongButtonHandler = requireHandler(
harness.commands,
"/reportlongbutton",
"/reportlongbutton",
);
unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm");
agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus");
argMenuHandler = requireHandler(harness.actions, /^openclaw_cmdarg/, "arg-menu action");
@@ -539,9 +563,20 @@ describe("Slack native command argument menus", () => {
expect(element?.confirm).toBeTruthy();
});
it("falls back to buttons when static_select value limit would be exceeded", async () => {
it("uses static_select when encoded values fit Slack option limits", async () => {
const firstElement = await getFirstActionElementFromCommand(reportLongHandler);
expect(firstElement?.type).toBe("static_select");
expect(firstElement?.confirm).toBeTruthy();
});
it("truncates button labels when static_select value limit would be exceeded", async () => {
const firstElement = (await getFirstActionElementFromCommand(reportLongButtonHandler)) as
| { type?: string; text?: { text?: string }; value?: string; confirm?: unknown }
| undefined;
expect(firstElement?.type).toBe("button");
expect(firstElement?.text?.text).toHaveLength(75);
expect(firstElement?.text?.text?.endsWith("…")).toBe(true);
expect(firstElement?.value?.length).toBeGreaterThan(150);
expect(firstElement?.confirm).toBeTruthy();
});

View File

@@ -52,7 +52,9 @@ const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5;
const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3;
const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
const SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX = 75;
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 150;
const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75;
const SLACK_HEADER_TEXT_MAX = 150;
let slashCommandsRuntimePromise: Promise<typeof import("./slash-commands.runtime.js")> | null =
null;
@@ -217,7 +219,10 @@ function parseSlackCommandArgValue(raw?: string | null): {
function buildSlackArgMenuOptions(choices: EncodedMenuChoice[]) {
return choices.map((choice) => ({
text: { type: "plain_text", text: choice.label.slice(0, 75) },
text: {
type: "plain_text",
text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX),
},
value: choice.value,
}));
}
@@ -293,7 +298,10 @@ function buildSlackCommandArgMenuBlocks(params: {
elements: choices.map((choice, colIndex) => ({
type: "button",
action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`,
text: { type: "plain_text", text: choice.label },
text: {
type: "plain_text",
text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_BUTTON_TEXT_MAX),
},
value: choice.value,
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
})),