Slack: preserve slash options receiver binding

This commit is contained in:
Vignesh Natarajan
2026-02-21 20:01:26 -08:00
parent 2e9ee22a9c
commit c51c2a2dca
3 changed files with 68 additions and 13 deletions

View File

@@ -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.

View File

@@ -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<string, (args: unknown) => Promise<void>>();
const actions = new Map<string, (args: unknown) => Promise<void>>();
const options = new Map<string, (args: unknown) => Promise<void>>();
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
const app = {
client: { chat: { postEphemeral } },
command: (name: string, handler: (args: unknown) => Promise<void>) => {
commands.set(name, handler);
},
action: (id: string, handler: (args: unknown) => Promise<void>) => {
actions.set(id, handler);
},
options: function (this: unknown, id: string, handler: (args: unknown) => Promise<void>) {
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);

View File

@@ -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<void>;
body: unknown;
}) => Promise<void>,
) => void;
}
).options;
if (typeof optionsHandler !== "function") {
const appWithOptions = ctx.app as unknown as {
options?: (
actionId: string,
handler: (args: {
ack: (payload: { options: unknown[] }) => Promise<void>;
body: unknown;
}) => Promise<void>,
) => 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 };