mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix: keep slack status reactions in tool-only rooms
This commit is contained in:
@@ -64,7 +64,7 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
|
||||
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.
|
||||
- Slack/directory: make `openclaw directory peers/groups list --channel slack` prefer token-backed live readers and return the connected Slack account from `directory self`, so valid Slack tokens no longer produce empty directory CLI results. Fixes #50776. Thanks @pjaillon.
|
||||
- Slack: keep the assistant typing status and temporary typing reaction active for group/channel turns that use message-tool-only visible replies, while still suppressing automatic source replies. Fixes #75877. Thanks @teosborne.
|
||||
- Slack: keep assistant typing status, temporary typing reactions, and status reactions active for group/channel turns that use message-tool-only visible replies, while still suppressing automatic source replies. Fixes #75877. Thanks @teosborne.
|
||||
- Slack: recover full inbound DM text from top-level rich-text blocks when Slack sends a shortened message preview, so long direct messages still reach the agent intact. Fixes #55358. Thanks @tonyjwinter.
|
||||
- Replies: strip legacy `[TOOL_CALL]{tool => ..., args => ...}[/TOOL_CALL]` pseudo-call text from user-facing replies and flag it in tool-call diagnostics instead of showing raw tool syntax in channels. Fixes #63610. Thanks @canh0chua.
|
||||
- WhatsApp: close long-lived web sockets through Baileys `end(error)` before falling back to raw websocket close, so listener teardown runs Baileys cleanup instead of leaving zombie sockets. Fixes #52442. Thanks @essendigitalgroup-cyber.
|
||||
|
||||
@@ -695,6 +695,39 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps status reactions for mentioned message-tool-only channel turns", async () => {
|
||||
replyMock.mockResolvedValue({ text: "quiet default reply" });
|
||||
slackTestState.config = {
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "group-mentions",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: {
|
||||
enabled: true,
|
||||
timing: { debounceMs: 0, doneHoldMs: 0, errorHoldMs: 0 },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
groupPolicy: "open",
|
||||
},
|
||||
},
|
||||
};
|
||||
mockGeneralChannelInfo();
|
||||
|
||||
await runMentionGatedChannelMessageAndFlush();
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(reactMock).toHaveBeenCalledWith({
|
||||
channel: "C1",
|
||||
timestamp: "456",
|
||||
name: "eyes",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the error reaction when dispatch fails before any reply is delivered", async () => {
|
||||
replyMock.mockRejectedValue(new Error("boom"));
|
||||
setMentionGatedAckConfig(true);
|
||||
|
||||
@@ -32,6 +32,16 @@ class TestSlackStreamNotDeliveredError extends Error {
|
||||
let mockedNativeStreaming = false;
|
||||
let mockedBlockStreamingEnabled: boolean | undefined = false;
|
||||
let capturedReplyOptions: { disableBlockStreaming?: boolean } | undefined;
|
||||
let capturedStatusReactionOptions: { enabled?: boolean; initialEmoji?: string } | undefined;
|
||||
const statusReactionControllerMock = {
|
||||
setQueued: vi.fn(async () => {}),
|
||||
setThinking: vi.fn(async () => {}),
|
||||
setTool: vi.fn(async () => {}),
|
||||
setError: vi.fn(async () => {}),
|
||||
setDone: vi.fn(async () => {}),
|
||||
clear: vi.fn(async () => {}),
|
||||
restoreInitial: vi.fn(async () => {}),
|
||||
};
|
||||
let mockedReplyThreadTs: string | undefined = THREAD_TS;
|
||||
let mockedReplyThreadTsSequence: Array<string | undefined> | undefined;
|
||||
let capturedTyping:
|
||||
@@ -87,6 +97,8 @@ function createPreparedSlackMessage(params?: {
|
||||
status: string;
|
||||
}) => Promise<void>;
|
||||
typingReaction?: string;
|
||||
ackReactionMessageTs?: string;
|
||||
ackReactionPromise?: Promise<boolean> | null;
|
||||
}) {
|
||||
return {
|
||||
ctx: {
|
||||
@@ -136,7 +148,8 @@ function createPreparedSlackMessage(params?: {
|
||||
historyKey: "history-key",
|
||||
preview: "",
|
||||
ackReactionValue: "eyes",
|
||||
ackReactionPromise: null,
|
||||
ackReactionMessageTs: params?.ackReactionMessageTs,
|
||||
ackReactionPromise: params?.ackReactionPromise ?? null,
|
||||
} as never;
|
||||
}
|
||||
|
||||
@@ -149,15 +162,10 @@ vi.mock("openclaw/plugin-sdk/channel-feedback", () => ({
|
||||
doneHoldMs: 0,
|
||||
errorHoldMs: 0,
|
||||
},
|
||||
createStatusReactionController: () => ({
|
||||
setQueued: async () => {},
|
||||
setThinking: async () => {},
|
||||
setTool: async () => {},
|
||||
setError: async () => {},
|
||||
setDone: async () => {},
|
||||
clear: async () => {},
|
||||
restoreInitial: async () => {},
|
||||
}),
|
||||
createStatusReactionController: (params: { enabled?: boolean; initialEmoji?: string }) => {
|
||||
capturedStatusReactionOptions = params;
|
||||
return statusReactionControllerMock;
|
||||
},
|
||||
logAckFailure: () => {},
|
||||
logTypingFailure: () => {},
|
||||
removeAckReactionAfterReply: () => {},
|
||||
@@ -408,9 +416,13 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
|
||||
stopSlackStreamMock.mockReset();
|
||||
reactSlackMessageMock.mockReset();
|
||||
removeSlackReactionMock.mockReset();
|
||||
for (const value of Object.values(statusReactionControllerMock)) {
|
||||
value.mockClear();
|
||||
}
|
||||
mockedNativeStreaming = false;
|
||||
mockedBlockStreamingEnabled = false;
|
||||
capturedReplyOptions = undefined;
|
||||
capturedStatusReactionOptions = undefined;
|
||||
capturedTyping = undefined;
|
||||
mockedReplyThreadTs = THREAD_TS;
|
||||
mockedReplyThreadTsSequence = undefined;
|
||||
@@ -520,6 +532,32 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Slack status reactions when channel replies are message-tool-only", async () => {
|
||||
await dispatchPreparedSlackMessage(
|
||||
createPreparedSlackMessage({
|
||||
cfg: {
|
||||
messages: {
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: { enabled: true },
|
||||
},
|
||||
},
|
||||
ctxPayload: { ChatType: "channel" },
|
||||
ackReactionMessageTs: "171234.111",
|
||||
ackReactionPromise: Promise.resolve(true),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(capturedReplyOptions?.disableBlockStreaming).toBe(true);
|
||||
expect(capturedStatusReactionOptions).toEqual(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
initialEmoji: "eyes",
|
||||
}),
|
||||
);
|
||||
expect(statusReactionControllerMock.setQueued).toHaveBeenCalledTimes(1);
|
||||
expect(statusReactionControllerMock.setDone).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("escapes Slack mrkdwn in tool progress preview labels", async () => {
|
||||
const draftStream = createDraftStreamStub();
|
||||
createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
|
||||
|
||||
@@ -306,7 +306,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
const incomingThreadTs = message.thread_ts;
|
||||
let didSetStatus = false;
|
||||
const statusReactionsEnabled =
|
||||
!sourceRepliesAreToolOnly &&
|
||||
Boolean(prepared.ackReactionPromise) &&
|
||||
Boolean(reactionMessageTs) &&
|
||||
cfg.messages?.statusReactions?.enabled !== false;
|
||||
|
||||
@@ -399,6 +399,42 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(prepared?.ackReactionPromise).toBeNull();
|
||||
});
|
||||
|
||||
it("primes Slack status reactions when channel replies are message-tool-only", async () => {
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
messages: {
|
||||
ackReaction: "eyes",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
statusReactions: { enabled: true },
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
groupPolicy: "open",
|
||||
replyToMode: "all",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "<@B1> hi",
|
||||
ts: "1.000",
|
||||
} as SlackMessageEvent);
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expect(prepared?.ackReactionMessageTs).toBe("1.000");
|
||||
expect(prepared?.ackReactionValue).toBe("eyes");
|
||||
expect(prepared?.ackReactionPromise).toBeTruthy();
|
||||
expect(await prepared!.ackReactionPromise).toBe(true);
|
||||
});
|
||||
|
||||
it("includes forwarded shared attachment text in raw body", async () => {
|
||||
const prepared = await prepareWithDefaultCtx(
|
||||
createSlackMessage({
|
||||
|
||||
@@ -563,7 +563,7 @@ export async function prepareSlackMessage(params: {
|
||||
const sourceRepliesAreToolOnly =
|
||||
resolveChannelSourceReplyDeliveryMode({ cfg, ctx: { ChatType: chatType } }) ===
|
||||
"message_tool_only";
|
||||
|
||||
const statusReactionsExplicitlyEnabled = cfg.messages?.statusReactions?.enabled === true;
|
||||
const shouldAckReaction = () =>
|
||||
Boolean(
|
||||
ackReaction &&
|
||||
@@ -580,7 +580,11 @@ export async function prepareSlackMessage(params: {
|
||||
);
|
||||
|
||||
const ackReactionMessageTs = message.ts;
|
||||
const shouldSendAckReaction = !sourceRepliesAreToolOnly && shouldAckReaction();
|
||||
const allowToolOnlyStatusReaction =
|
||||
statusReactionsExplicitlyEnabled &&
|
||||
(effectiveWasMentioned || mentionDecision.shouldBypassMention === true);
|
||||
const shouldSendAckReaction =
|
||||
shouldAckReaction() && (!sourceRepliesAreToolOnly || allowToolOnlyStatusReaction);
|
||||
const statusReactionsWillHandle =
|
||||
Boolean(ackReactionMessageTs) &&
|
||||
cfg.messages?.statusReactions?.enabled !== false &&
|
||||
|
||||
Reference in New Issue
Block a user