diff --git a/src/auto-reply/reply/reply-flow.test.ts b/src/auto-reply/reply/reply-flow.test.ts index b46cedc59aa..d7efa640b1c 100644 --- a/src/auto-reply/reply/reply-flow.test.ts +++ b/src/auto-reply/reply/reply-flow.test.ts @@ -627,7 +627,7 @@ describe("parseSlackDirectives", () => { text: "Approve", emoji: true, }, - value: "approve", + value: "reply_1_approve", }, { type: "button", @@ -637,7 +637,7 @@ describe("parseSlackDirectives", () => { text: "Reject", emoji: true, }, - value: "reject", + value: "reply_2_reject", }, ], }, @@ -670,7 +670,7 @@ describe("parseSlackDirectives", () => { text: "Alpha", emoji: true, }, - value: "alpha", + value: "reply_1_alpha", }, { text: { @@ -678,7 +678,7 @@ describe("parseSlackDirectives", () => { text: "Beta", emoji: true, }, - value: "beta", + value: "reply_2_beta", }, ], }, @@ -719,7 +719,7 @@ describe("parseSlackDirectives", () => { text: "Retry", emoji: true, }, - value: "retry", + value: "reply_1_retry", }, ], }, @@ -751,7 +751,7 @@ describe("parseSlackDirectives", () => { text: "Alpha", emoji: true, }, - value: "alpha", + value: "reply_1_alpha", }, ], }, @@ -776,7 +776,7 @@ describe("parseSlackDirectives", () => { text: "Retry", emoji: true, }, - value: "retry", + value: "reply_1_retry", }, ], }, @@ -858,6 +858,19 @@ describe("parseSlackDirectives", () => { }, }); }); + + it("ignores malformed existing Slack blocks during directive compilation", () => { + expect(() => + parseSlackDirectives({ + text: "Choose [[slack_buttons: Retry:retry]]", + channelData: { + slack: { + blocks: "{not json}", + }, + }, + }), + ).not.toThrow(); + }); }); function createDeferred() { diff --git a/src/auto-reply/reply/reply-utils.test.ts b/src/auto-reply/reply/reply-utils.test.ts index 4a15cf80064..88f092bf1e5 100644 --- a/src/auto-reply/reply/reply-utils.test.ts +++ b/src/auto-reply/reply/reply-utils.test.ts @@ -193,7 +193,7 @@ describe("normalizeReplyPayload", () => { text: "Retry", emoji: true, }, - value: "retry", + value: "reply_1_retry", }, { type: "button", @@ -203,7 +203,7 @@ describe("normalizeReplyPayload", () => { text: "Ignore", emoji: true, }, - value: "ignore", + value: "reply_2_ignore", }, ], }, diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index d5ecbdd6f2f..5a0405da22b 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -230,6 +230,26 @@ describe("routeReply", () => { ); }); + it("does not bypass the empty-reply guard for invalid Slack blocks", async () => { + mocks.sendMessageSlack.mockClear(); + const res = await routeReply({ + payload: { + text: " ", + channelData: { + slack: { + blocks: " ", + }, + }, + }, + channel: "slack", + to: "channel:C123", + cfg: {} as never, + }); + + expect(res.ok).toBe(true); + expect(mocks.sendMessageSlack).not.toHaveBeenCalled(); + }); + it("does not derive responsePrefix from agent identity when routing", async () => { mocks.sendMessageSlack.mockClear(); const cfg = { diff --git a/src/auto-reply/reply/route-reply.ts b/src/auto-reply/reply/route-reply.ts index e915308f39a..8b3319698b2 100644 --- a/src/auto-reply/reply/route-reply.ts +++ b/src/auto-reply/reply/route-reply.ts @@ -12,6 +12,7 @@ import { resolveEffectiveMessagesConfig } from "../../agents/identity.js"; import { normalizeChannelId } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js"; +import { parseSlackBlocksInput } from "../../slack/blocks-input.js"; import { isSlackInteractiveRepliesEnabled } from "../../slack/interactive-replies.js"; import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js"; import type { OriginatingChannelType } from "../templating.js"; @@ -109,14 +110,22 @@ export async function routeReply(params: RouteReplyParams): Promise ({ + elements: choices.map((choice, choiceIndex) => ({ type: "button", action_id: SLACK_REPLY_BUTTON_ACTION_ID, text: { @@ -90,7 +99,7 @@ function buildButtonsBlock(raw: string, index: number): SlackBlock | null { text: truncateSlackText(choice.label, SLACK_PLAIN_TEXT_MAX), emoji: true, }, - value: truncateSlackText(choice.value, SLACK_OPTION_VALUE_MAX), + value: buildSlackReplyChoiceToken(choice.value, choiceIndex + 1), })), }; } @@ -121,13 +130,13 @@ function buildSelectBlock(raw: string, index: number): SlackBlock | null { text: truncateSlackText(placeholder, SLACK_PLAIN_TEXT_MAX), emoji: true, }, - options: choices.map((choice) => ({ + options: choices.map((choice, choiceIndex) => ({ text: { type: "plain_text", text: truncateSlackText(choice.label, SLACK_PLAIN_TEXT_MAX), emoji: true, }, - value: truncateSlackText(choice.value, SLACK_OPTION_VALUE_MAX), + value: buildSlackReplyChoiceToken(choice.value, choiceIndex + 1), })), }, ], @@ -136,8 +145,12 @@ function buildSelectBlock(raw: string, index: number): SlackBlock | null { function readExistingSlackBlocks(payload: ReplyPayload): SlackBlock[] { const slackData = payload.channelData?.slack as SlackChannelData | undefined; - const blocks = parseSlackBlocksInput(slackData?.blocks) as SlackBlock[] | undefined; - return blocks ?? []; + try { + const blocks = parseSlackBlocksInput(slackData?.blocks) as SlackBlock[] | undefined; + return blocks ?? []; + } catch { + return []; + } } export function hasSlackDirectives(text: string): boolean {