fix(slack): guard status reactions without ts

This commit is contained in:
Frank Yang 2026-03-29 10:34:19 +08:00
parent 9217053930
commit 9ac8e35c4b
4 changed files with 41 additions and 5 deletions

View File

@ -190,14 +190,17 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
replyToMode: prepared.replyToMode,
});
const reactionMessageTs = prepared.ackReactionMessageTs;
const messageTs = message.ts ?? message.event_ts;
const incomingThreadTs = message.thread_ts;
let didSetStatus = false;
const statusReactionsEnabled =
Boolean(prepared.ackReactionPromise) && cfg.messages?.statusReactions?.enabled !== false;
Boolean(prepared.ackReactionPromise) &&
Boolean(reactionMessageTs) &&
cfg.messages?.statusReactions?.enabled !== false;
const slackStatusAdapter: StatusReactionAdapter = {
setReaction: async (emoji) => {
await reactSlackMessage(message.channel, message.ts ?? "", toSlackEmojiName(emoji), {
await reactSlackMessage(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), {
token: ctx.botToken,
client: ctx.app.client,
}).catch((err) => {
@ -208,7 +211,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
},
removeReaction: async (emoji) => {
await removeSlackReaction(message.channel, message.ts ?? "", toSlackEmojiName(emoji), {
await removeSlackReaction(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), {
token: ctx.botToken,
client: ctx.app.client,
}).catch((err) => {

View File

@ -214,6 +214,33 @@ describe("slack prepareSlackMessage inbound contract", () => {
expectInboundContextContract(prepared!.ctxPayload as any);
});
it("does not enable Slack status reactions when the message timestamp is missing", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {
messages: {
ackReaction: "👀",
ackReactionScope: "all",
statusReactions: { enabled: true },
},
channels: { slack: { enabled: true } },
} as OpenClawConfig,
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
channel: "D123",
channel_type: "im",
user: "U1",
text: "hi",
event_ts: "1.000",
} as SlackMessageEvent);
expect(prepared).toBeTruthy();
expect(prepared?.ackReactionMessageTs).toBeUndefined();
expect(prepared?.ackReactionPromise).toBeNull();
});
it("includes forwarded shared attachment text in raw body", async () => {
const prepared = await prepareWithDefaultCtx(
createSlackMessage({

View File

@ -556,7 +556,9 @@ export async function prepareSlackMessage(params: {
const ackReactionMessageTs = message.ts;
const statusReactionsWillHandle =
cfg.messages?.statusReactions?.enabled !== false && shouldAckReaction();
Boolean(ackReactionMessageTs) &&
cfg.messages?.statusReactions?.enabled !== false &&
shouldAckReaction();
const ackReactionPromise =
!statusReactionsWillHandle && shouldAckReaction() && ackReactionMessageTs && ackReactionValue
? reactSlackMessage(message.channel, ackReactionMessageTs, ackReactionValue, {

View File

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
createStatusReactionController,
DEFAULT_EMOJIS,
@ -36,6 +36,10 @@ describe("Slack status reaction lifecycle", () => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("queued -> thinking -> tool -> done -> clear", async () => {
const { adapter, active, log } = createSlackMockAdapter();
const ctrl = createStatusReactionController({