

























@@ -0,0 +1,150 @@
1+import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { createSlackSendTestClient, installSlackBlockTestMocks } from "./blocks.test-helpers.js";
4+5+vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
6+logVerbose: vi.fn(),
7+danger: (message: string) => message,
8+shouldLogVerbose: () => false,
9+}));
10+11+installSlackBlockTestMocks();
12+const { sendMessageSlack } = await import("./send.js");
13+14+type SlackMissingScopeError = Error & {
15+data?: {
16+error?: string;
17+needed?: string;
18+response_metadata?: { scopes?: string[]; acceptedScopes?: string[] };
19+};
20+};
21+22+function buildMissingScopeError(overrides?: {
23+needed?: string;
24+scopes?: string[];
25+acceptedScopes?: string[];
26+}): SlackMissingScopeError {
27+const err = new Error("missing_scope") as SlackMissingScopeError;
28+const response_metadata =
29+overrides?.scopes || overrides?.acceptedScopes
30+ ? {
31+ ...(overrides?.scopes ? { scopes: overrides.scopes } : {}),
32+ ...(overrides?.acceptedScopes ? { acceptedScopes: overrides.acceptedScopes } : {}),
33+}
34+ : undefined;
35+err.data = {
36+error: "missing_scope",
37+ ...(overrides?.needed != null ? { needed: overrides.needed } : {}),
38+ ...(response_metadata ? { response_metadata } : {}),
39+};
40+return err;
41+}
42+43+describe("sendMessageSlack customize-scope fallback", () => {
44+beforeEach(() => {
45+vi.mocked(logVerbose).mockClear();
46+});
47+48+it("retries without identity when needed contains chat:write.customize", async () => {
49+const client = createSlackSendTestClient();
50+vi.mocked(client.chat.postMessage)
51+.mockRejectedValueOnce(buildMissingScopeError({ needed: "chat:write.customize" }))
52+.mockResolvedValueOnce({ ts: "171234.567" });
53+54+const result = await sendMessageSlack("channel:C123", "hello", {
55+token: "xoxb-test",
56+ client,
57+identity: { username: "Bot", iconUrl: "https://example.com/bot.png" },
58+});
59+60+expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
61+const [firstCall] = vi.mocked(client.chat.postMessage).mock.calls[0];
62+const [secondCall] = vi.mocked(client.chat.postMessage).mock.calls[1];
63+expect(firstCall).toEqual(
64+expect.objectContaining({
65+username: "Bot",
66+icon_url: "https://example.com/bot.png",
67+}),
68+);
69+expect(secondCall).not.toHaveProperty("username");
70+expect(secondCall).not.toHaveProperty("icon_url");
71+expect(secondCall).not.toHaveProperty("icon_emoji");
72+expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
73+"slack send: missing chat:write.customize, retrying without custom identity",
74+);
75+expect(result.messageId).toBe("171234.567");
76+});
77+78+it("retries when chat:write.customize appears only in response_metadata.acceptedScopes", async () => {
79+const client = createSlackSendTestClient();
80+vi.mocked(client.chat.postMessage)
81+.mockRejectedValueOnce(
82+buildMissingScopeError({ acceptedScopes: ["chat:write", "chat:write.customize"] }),
83+)
84+.mockResolvedValueOnce({ ts: "171234.567" });
85+86+await sendMessageSlack("channel:C123", "hello", {
87+token: "xoxb-test",
88+ client,
89+identity: { iconEmoji: ":robot_face:" },
90+});
91+92+expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
93+const [secondCall] = vi.mocked(client.chat.postMessage).mock.calls[1];
94+expect(secondCall).not.toHaveProperty("icon_emoji");
95+expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
96+"slack send: missing chat:write.customize, retrying without custom identity",
97+);
98+});
99+100+it("retries when chat:write.customize appears only in response_metadata.scopes", async () => {
101+const client = createSlackSendTestClient();
102+vi.mocked(client.chat.postMessage)
103+.mockRejectedValueOnce(buildMissingScopeError({ scopes: ["chat:write.customize"] }))
104+.mockResolvedValueOnce({ ts: "171234.567" });
105+106+await sendMessageSlack("channel:C123", "hello", {
107+token: "xoxb-test",
108+ client,
109+identity: { username: "Bot" },
110+});
111+112+expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
113+expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
114+"slack send: missing chat:write.customize, retrying without custom identity",
115+);
116+});
117+118+it("rethrows missing_scope errors that reference a different scope", async () => {
119+const client = createSlackSendTestClient();
120+const err = buildMissingScopeError({ needed: "channels:history" });
121+vi.mocked(client.chat.postMessage).mockRejectedValueOnce(err);
122+123+await expect(
124+sendMessageSlack("channel:C123", "hello", {
125+token: "xoxb-test",
126+ client,
127+identity: { username: "Bot" },
128+}),
129+).rejects.toBe(err);
130+131+expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
132+expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
133+});
134+135+it("rethrows customize-scope errors when identity is empty", async () => {
136+const client = createSlackSendTestClient();
137+const err = buildMissingScopeError({ needed: "chat:write.customize" });
138+vi.mocked(client.chat.postMessage).mockRejectedValueOnce(err);
139+140+await expect(
141+sendMessageSlack("channel:C123", "hello", {
142+token: "xoxb-test",
143+ client,
144+}),
145+).rejects.toBe(err);
146+147+expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
148+expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
149+});
150+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。