diff --git a/CHANGELOG.md b/CHANGELOG.md index 911754b76aa..21babf4e1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Slack/Slash commands: preserve the Bolt app receiver when registering external select options handlers so monitor startup does not crash on runtimes that require bound `app.options` calls. (#23209) Thanks @0xgaia. - Agents/Ollama: preserve unsafe integer tool-call arguments as exact strings during NDJSON parsing, preventing large numeric IDs from being rounded before tool execution. (#23170) Thanks @BestJoester. - Cron/Gateway: keep `cron.list` and `cron.status` responsive during startup catch-up by avoiding a long-held cron lock while missed jobs execute. (#23106) Thanks @jayleekr. - Gateway/Config reload: compare array-valued config paths structurally during diffing so unchanged `memory.qmd.paths` and `memory.qmd.scope.rules` no longer trigger false restart-required reloads. (#23185) Thanks @rex05ai. diff --git a/src/slack/monitor/slash.test.ts b/src/slack/monitor/slash.test.ts index 4934589a167..53fa613b94d 100644 --- a/src/slack/monitor/slash.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -370,6 +370,62 @@ describe("Slack native command argument menus", () => { harness.postEphemeral.mockClear(); }); + it("registers options handlers without losing app receiver binding", async () => { + const commands = new Map Promise>(); + const actions = new Map Promise>(); + const options = new Map Promise>(); + const postEphemeral = vi.fn().mockResolvedValue({ ok: true }); + const app = { + client: { chat: { postEphemeral } }, + command: (name: string, handler: (args: unknown) => Promise) => { + commands.set(name, handler); + }, + action: (id: string, handler: (args: unknown) => Promise) => { + actions.set(id, handler); + }, + options: function (this: unknown, id: string, handler: (args: unknown) => Promise) { + expect(this).toBe(app); + options.set(id, handler); + }, + }; + const ctx = { + cfg: { commands: { native: true, nativeSkills: false } }, + runtime: {}, + botToken: "bot-token", + botUserId: "bot", + teamId: "T1", + allowFrom: ["*"], + dmEnabled: true, + dmPolicy: "open", + groupDmEnabled: false, + groupDmChannels: [], + defaultRequireMention: true, + groupPolicy: "open", + useAccessGroups: false, + channelsConfig: undefined, + slashCommand: { + enabled: true, + name: "openclaw", + ephemeral: true, + sessionPrefix: "slack:slash", + }, + textLimit: 4000, + app, + isChannelAllowed: () => true, + resolveChannelName: async () => ({ name: "dm", type: "im" }), + resolveUserName: async () => ({ name: "Ada" }), + } as unknown; + const account = { + accountId: "acct", + config: { commands: { native: true, nativeSkills: false } }, + } as unknown; + + await registerCommands(ctx, account); + expect(commands.size).toBeGreaterThan(0); + expect(actions.has("openclaw_cmdarg")).toBe(true); + expect(options.has("openclaw_cmdarg")).toBe(true); + }); + it("shows a button menu when required args are omitted", async () => { const { respond } = await runCommandHandler(usageHandler); const actions = expectArgMenuLayout(respond); diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index bc379db5924..27af729dbf0 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -734,21 +734,19 @@ export async function registerSlackMonitorSlashCommands(params: { } const registerArgOptions = () => { - const optionsHandler = ( - ctx.app as unknown as { - options?: ( - actionId: string, - handler: (args: { - ack: (payload: { options: unknown[] }) => Promise; - body: unknown; - }) => Promise, - ) => void; - } - ).options; - if (typeof optionsHandler !== "function") { + const appWithOptions = ctx.app as unknown as { + options?: ( + actionId: string, + handler: (args: { + ack: (payload: { options: unknown[] }) => Promise; + body: unknown; + }) => Promise, + ) => void; + }; + if (typeof appWithOptions.options !== "function") { return; } - optionsHandler(SLACK_COMMAND_ARG_ACTION_ID, async ({ ack, body }) => { + appWithOptions.options(SLACK_COMMAND_ARG_ACTION_ID, async ({ ack, body }) => { const typedBody = body as { value?: string; user?: { id?: string };