diff --git a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts index 80b01c65ae63..b2ad2136431f 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts @@ -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({ diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 349b0aa67638..1aa2aa3d9181 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -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) => {