fix(slack): log typing reaction errors instead of silently swallowing them (#103303)

* fix(slack): log typing reaction errors instead of silently swallowing them

The typing reaction (emoji) add and remove calls both use empty catch
handlers, silently discarding all errors. If the bot token lacks
reactions:write scope, the operator has no way to diagnose why the
configured typing reaction never appears.

Replace the empty catch with logVerbose calls that record the Slack
error, matching the existing logging pattern in the same file.

* test(slack): cover typing reaction failures

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
zw-xysk
2026-07-12 16:20:56 +08:00
committed by GitHub
parent 218dcd815a
commit 1cbb67668f
2 changed files with 50 additions and 3 deletions
@@ -28,6 +28,7 @@ const stopSlackStreamMock = vi.fn(async (_params?: unknown) => ({}) as { message
const emitSlackMessageSentHooksMock = vi.fn(() => {});
const reactSlackMessageMock = vi.fn(async () => {});
const removeSlackReactionMock = vi.fn(async () => {});
const logVerboseMock = vi.fn();
class TestSlackStreamNotDeliveredError extends Error {
readonly pendingText: string;
readonly slackCode: string;
@@ -200,6 +201,14 @@ function requireCapturedTyping() {
return capturedTyping;
}
function createSlackPlatformError(error: string, details?: { needed?: string; provided?: string }) {
// Mirrors @slack/web-api 7.18.0 platformErrorFromResult: message plus structured result data.
return Object.assign(new Error(`An API error occurred: ${error}`), {
code: "slack_webapi_platform_error",
data: { ok: false, error, ...details },
});
}
function requireCapturedItemEventHandler() {
const handler = capturedReplyOptions?.onItemEvent;
if (!handler) {
@@ -819,7 +828,7 @@ vi.mock("openclaw/plugin-sdk/reply-payload", () => ({
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
danger: (message: string) => message,
logVerbose: () => {},
logVerbose: logVerboseMock,
shouldLogVerbose: () => false,
}));
@@ -1258,6 +1267,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
stopSlackStreamMock.mockReset();
reactSlackMessageMock.mockReset();
removeSlackReactionMock.mockReset();
logVerboseMock.mockReset();
for (const value of Object.values(statusReactionControllerMock)) {
value.mockClear();
}
@@ -1982,6 +1992,39 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
);
});
it("logs the formatted Slack error when adding the typing reaction fails", async () => {
reactSlackMessageMock.mockRejectedValueOnce(
createSlackPlatformError("missing_scope", {
needed: "reactions:write",
provided: "chat:write",
}),
);
await dispatchPreparedSlackMessage(
createPreparedSlackMessage({ typingReaction: "hourglass_flowing_sand" }),
);
await expect(requireCapturedTyping().start()).resolves.toBeUndefined();
expect(logVerboseMock).toHaveBeenCalledWith(
"slack send: typing reaction failed: An API error occurred: missing_scope; code: slack_webapi_platform_error; slack error: missing_scope; needed: reactions:write; provided: chat:write",
);
});
it("logs the formatted Slack error when removing the typing reaction fails", async () => {
removeSlackReactionMock.mockRejectedValueOnce(createSlackPlatformError("invalid_auth"));
await dispatchPreparedSlackMessage(
createPreparedSlackMessage({ typingReaction: "hourglass_flowing_sand" }),
);
const typing = requireCapturedTyping();
await typing.start();
await expect(typing.stop?.()).resolves.toBeUndefined();
expect(logVerboseMock).toHaveBeenCalledWith(
"slack send: typing reaction removal failed: An API error occurred: invalid_auth; code: slack_webapi_platform_error; slack error: invalid_auth",
);
});
it("keeps Slack status reactions when channel replies are message-tool-only", async () => {
await dispatchPreparedSlackMessage(
createPreparedSlackMessage({
@@ -614,7 +614,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
await reactSlackMessage(message.channel, message.ts, typingReaction, {
token: ctx.botToken,
client: slackClient,
}).catch(() => {});
}).catch((err: unknown) => {
logVerbose(`slack send: typing reaction failed: ${formatSlackError(err)}`);
});
}
},
stop: async () => {
@@ -632,7 +634,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
await removeSlackReaction(message.channel, message.ts, typingReaction, {
token: ctx.botToken,
client: slackClient,
}).catch(() => {});
}).catch((err: unknown) => {
logVerbose(`slack send: typing reaction removal failed: ${formatSlackError(err)}`);
});
}
},
onStartError: (err) => {