mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
test(slack): cover send.ts customize-scope fallback retry path (#69009)
Adds 5 vitest cases for postSlackMessageBestEffort's silent retry behavior when Slack rejects a chat:write.customize-identity post: - Retry on err.data.needed matching chat:write.customize - Retry on chat:write.customize in response_metadata.acceptedScopes - Retry on chat:write.customize in response_metadata.scopes - Rethrow on different missing_scope (e.g. channels:history) - Rethrow when identity is empty (hasCustomIdentity returns false)
This commit is contained in:
150
extensions/slack/src/send.identity-fallback.test.ts
Normal file
150
extensions/slack/src/send.identity-fallback.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSlackSendTestClient, installSlackBlockTestMocks } from "./blocks.test-helpers.js";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
logVerbose: vi.fn(),
|
||||
danger: (message: string) => message,
|
||||
shouldLogVerbose: () => false,
|
||||
}));
|
||||
|
||||
installSlackBlockTestMocks();
|
||||
const { sendMessageSlack } = await import("./send.js");
|
||||
|
||||
type SlackMissingScopeError = Error & {
|
||||
data?: {
|
||||
error?: string;
|
||||
needed?: string;
|
||||
response_metadata?: { scopes?: string[]; acceptedScopes?: string[] };
|
||||
};
|
||||
};
|
||||
|
||||
function buildMissingScopeError(overrides?: {
|
||||
needed?: string;
|
||||
scopes?: string[];
|
||||
acceptedScopes?: string[];
|
||||
}): SlackMissingScopeError {
|
||||
const err = new Error("missing_scope") as SlackMissingScopeError;
|
||||
const response_metadata =
|
||||
overrides?.scopes || overrides?.acceptedScopes
|
||||
? {
|
||||
...(overrides?.scopes ? { scopes: overrides.scopes } : {}),
|
||||
...(overrides?.acceptedScopes ? { acceptedScopes: overrides.acceptedScopes } : {}),
|
||||
}
|
||||
: undefined;
|
||||
err.data = {
|
||||
error: "missing_scope",
|
||||
...(overrides?.needed != null ? { needed: overrides.needed } : {}),
|
||||
...(response_metadata ? { response_metadata } : {}),
|
||||
};
|
||||
return err;
|
||||
}
|
||||
|
||||
describe("sendMessageSlack customize-scope fallback", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(logVerbose).mockClear();
|
||||
});
|
||||
|
||||
it("retries without identity when needed contains chat:write.customize", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
vi.mocked(client.chat.postMessage)
|
||||
.mockRejectedValueOnce(buildMissingScopeError({ needed: "chat:write.customize" }))
|
||||
.mockResolvedValueOnce({ ts: "171234.567" });
|
||||
|
||||
const result = await sendMessageSlack("channel:C123", "hello", {
|
||||
token: "xoxb-test",
|
||||
client,
|
||||
identity: { username: "Bot", iconUrl: "https://example.com/bot.png" },
|
||||
});
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
|
||||
const [firstCall] = vi.mocked(client.chat.postMessage).mock.calls[0];
|
||||
const [secondCall] = vi.mocked(client.chat.postMessage).mock.calls[1];
|
||||
expect(firstCall).toEqual(
|
||||
expect.objectContaining({
|
||||
username: "Bot",
|
||||
icon_url: "https://example.com/bot.png",
|
||||
}),
|
||||
);
|
||||
expect(secondCall).not.toHaveProperty("username");
|
||||
expect(secondCall).not.toHaveProperty("icon_url");
|
||||
expect(secondCall).not.toHaveProperty("icon_emoji");
|
||||
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
|
||||
"slack send: missing chat:write.customize, retrying without custom identity",
|
||||
);
|
||||
expect(result.messageId).toBe("171234.567");
|
||||
});
|
||||
|
||||
it("retries when chat:write.customize appears only in response_metadata.acceptedScopes", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
vi.mocked(client.chat.postMessage)
|
||||
.mockRejectedValueOnce(
|
||||
buildMissingScopeError({ acceptedScopes: ["chat:write", "chat:write.customize"] }),
|
||||
)
|
||||
.mockResolvedValueOnce({ ts: "171234.567" });
|
||||
|
||||
await sendMessageSlack("channel:C123", "hello", {
|
||||
token: "xoxb-test",
|
||||
client,
|
||||
identity: { iconEmoji: ":robot_face:" },
|
||||
});
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
|
||||
const [secondCall] = vi.mocked(client.chat.postMessage).mock.calls[1];
|
||||
expect(secondCall).not.toHaveProperty("icon_emoji");
|
||||
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
|
||||
"slack send: missing chat:write.customize, retrying without custom identity",
|
||||
);
|
||||
});
|
||||
|
||||
it("retries when chat:write.customize appears only in response_metadata.scopes", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
vi.mocked(client.chat.postMessage)
|
||||
.mockRejectedValueOnce(buildMissingScopeError({ scopes: ["chat:write.customize"] }))
|
||||
.mockResolvedValueOnce({ ts: "171234.567" });
|
||||
|
||||
await sendMessageSlack("channel:C123", "hello", {
|
||||
token: "xoxb-test",
|
||||
client,
|
||||
identity: { username: "Bot" },
|
||||
});
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(
|
||||
"slack send: missing chat:write.customize, retrying without custom identity",
|
||||
);
|
||||
});
|
||||
|
||||
it("rethrows missing_scope errors that reference a different scope", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const err = buildMissingScopeError({ needed: "channels:history" });
|
||||
vi.mocked(client.chat.postMessage).mockRejectedValueOnce(err);
|
||||
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hello", {
|
||||
token: "xoxb-test",
|
||||
client,
|
||||
identity: { username: "Bot" },
|
||||
}),
|
||||
).rejects.toBe(err);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rethrows customize-scope errors when identity is empty", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
const err = buildMissingScopeError({ needed: "chat:write.customize" });
|
||||
vi.mocked(client.chat.postMessage).mockRejectedValueOnce(err);
|
||||
|
||||
await expect(
|
||||
sendMessageSlack("channel:C123", "hello", {
|
||||
token: "xoxb-test",
|
||||
client,
|
||||
}),
|
||||
).rejects.toBe(err);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user