Skip to content

Commit 31e5cd6

Browse files
martingarramonsteipete
authored andcommitted
fix(slack): surface silent errors in thread starter/history fetch
Fixes #62571. `resolveSlackThreadStarter` and `resolveSlackThreadHistory` in `extensions/slack/src/monitor/media.ts` swallowed ALL errors with bare `catch {}` blocks — auth failures, rate-limit rejections, scope errors, and network blips all mapped to the same silent `null` / `[]` fallback. Operators had no way to distinguish "genuinely empty thread" from "Slack rejected our call". Replaces both bare catches with `logVerbose` calls that include the channel, thread ts, and error message. Behavior is preserved — callers still receive `null` / `[]` — but the failure reason now shows up in verbose logs, matching the pattern already used elsewhere in the Slack extension (see `monitor/context.ts:285`, `send.ts:140`, `actions.ts:49`). Testing: - New `describe("resolveSlackThreadStarter", ...)` block with 4 tests (previously uncovered): success path, empty-text skip, Error throw surfaces via logVerbose with channel/ts/reason, non-Error throw value surfaces via String(err). - Existing `resolveSlackThreadHistory` throws test upgraded to assert the logVerbose call with channel/ts/reason. - `pnpm vitest run extensions/slack/src/monitor/media.test.ts` → 35 passed (31 previous + 4 new).
1 parent e7d33b4 commit 31e5cd6

2 files changed

Lines changed: 103 additions & 3 deletions

File tree

extensions/slack/src/monitor/media.test.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as ssrf from "openclaw/plugin-sdk/infra-runtime";
22
import * as mediaFetch from "openclaw/plugin-sdk/media-runtime";
33
import type { SavedMedia } from "openclaw/plugin-sdk/media-runtime";
44
import * as mediaStore from "openclaw/plugin-sdk/media-runtime";
5+
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
56
import { type FetchMock, withFetchPreconnect } from "openclaw/plugin-sdk/testing";
67
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
78
import { mockPinnedHostnameResolution } from "../../../../src/test-helpers/ssrf.js";
@@ -10,8 +11,16 @@ import {
1011
resolveSlackAttachmentContent,
1112
resolveSlackMedia,
1213
resolveSlackThreadHistory,
14+
resolveSlackThreadStarter,
15+
resetSlackThreadStarterCacheForTest,
1316
} from "./media.js";
1417

18+
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
19+
logVerbose: vi.fn(),
20+
danger: (message: string) => message,
21+
shouldLogVerbose: () => false,
22+
}));
23+
1524
// Store original fetch
1625
const originalFetch = globalThis.fetch;
1726
let mockFetch: ReturnType<typeof vi.fn<FetchMock>>;
@@ -843,7 +852,8 @@ describe("resolveSlackThreadHistory", () => {
843852
expect(replies).not.toHaveBeenCalled();
844853
});
845854

846-
it("returns empty when Slack API throws", async () => {
855+
it("returns empty and surfaces the error via logVerbose when Slack API throws", async () => {
856+
vi.mocked(logVerbose).mockClear();
847857
const replies = vi.fn().mockRejectedValueOnce(new Error("slack down"));
848858
const client = {
849859
conversations: { replies },
@@ -857,5 +867,88 @@ describe("resolveSlackThreadHistory", () => {
857867
});
858868

859869
expect(result).toEqual([]);
870+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
871+
expect.stringContaining("slack thread history fetch failed"),
872+
);
873+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("slack down"));
874+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("channel=C1"));
875+
});
876+
});
877+
878+
describe("resolveSlackThreadStarter", () => {
879+
beforeEach(() => {
880+
resetSlackThreadStarterCacheForTest();
881+
vi.mocked(logVerbose).mockClear();
882+
});
883+
884+
it("returns the starter message when the Slack API succeeds", async () => {
885+
const replies = vi.fn().mockResolvedValueOnce({
886+
messages: [{ text: "hello thread", user: "U1", ts: "1.000" }],
887+
});
888+
const client = {
889+
conversations: { replies },
890+
} as unknown as Parameters<typeof resolveSlackThreadStarter>[0]["client"];
891+
892+
const result = await resolveSlackThreadStarter({
893+
channelId: "C1",
894+
threadTs: "1.000",
895+
client,
896+
});
897+
898+
expect(result).toEqual({ text: "hello thread", userId: "U1", botId: undefined, ts: "1.000", files: undefined });
899+
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
900+
});
901+
902+
it("returns null when the starter message has no text and no files", async () => {
903+
const replies = vi.fn().mockResolvedValueOnce({ messages: [{ text: " ", user: "U1" }] });
904+
const client = {
905+
conversations: { replies },
906+
} as unknown as Parameters<typeof resolveSlackThreadStarter>[0]["client"];
907+
908+
const result = await resolveSlackThreadStarter({
909+
channelId: "C1",
910+
threadTs: "1.000",
911+
client,
912+
});
913+
914+
expect(result).toBeNull();
915+
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
916+
});
917+
918+
it("returns null and surfaces the error via logVerbose when Slack API throws", async () => {
919+
const replies = vi.fn().mockRejectedValueOnce(new Error("not_in_channel"));
920+
const client = {
921+
conversations: { replies },
922+
} as unknown as Parameters<typeof resolveSlackThreadStarter>[0]["client"];
923+
924+
const result = await resolveSlackThreadStarter({
925+
channelId: "C42",
926+
threadTs: "9.999",
927+
client,
928+
});
929+
930+
expect(result).toBeNull();
931+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
932+
expect.stringContaining("slack thread starter fetch failed"),
933+
);
934+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("not_in_channel"));
935+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("channel=C42"));
936+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("ts=9.999"));
937+
});
938+
939+
it("surfaces non-Error thrown values as String(err) via logVerbose", async () => {
940+
const replies = vi.fn().mockRejectedValueOnce("rate_limited");
941+
const client = {
942+
conversations: { replies },
943+
} as unknown as Parameters<typeof resolveSlackThreadStarter>[0]["client"];
944+
945+
const result = await resolveSlackThreadStarter({
946+
channelId: "C1",
947+
threadTs: "1.000",
948+
client,
949+
});
950+
951+
expect(result).toBeNull();
952+
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("rate_limited"));
860953
});
861954
});

extensions/slack/src/monitor/media.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { FetchLike } from "openclaw/plugin-sdk/media-runtime";
55
import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime";
66
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
77
import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
8+
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
89
import {
910
normalizeLowercaseStringOrEmpty,
1011
normalizeOptionalLowercaseString,
@@ -447,7 +448,10 @@ export async function resolveSlackThreadStarter(params: {
447448
});
448449
evictThreadStarterCache();
449450
return starter;
450-
} catch {
451+
} catch (err) {
452+
logVerbose(
453+
`slack thread starter fetch failed channel=${params.channelId} ts=${params.threadTs}: ${err instanceof Error ? err.message : String(err)}`,
454+
);
451455
return null;
452456
}
453457
}
@@ -539,7 +543,10 @@ export async function resolveSlackThreadHistory(params: {
539543
ts: msg.ts,
540544
files: msg.files,
541545
}));
542-
} catch {
546+
} catch (err) {
547+
logVerbose(
548+
`slack thread history fetch failed channel=${params.channelId} ts=${params.threadTs}: ${err instanceof Error ? err.message : String(err)}`,
549+
);
543550
return [];
544551
}
545552
}

0 commit comments

Comments
 (0)