import type { OpenClawConfig } from "openclaw/plugin-sdk/slack"; import { describe, expect, it, vi } from "vitest"; const handleSlackActionMock = vi.fn(); vi.mock("./runtime.js", () => ({ getSlackRuntime: () => ({ channel: { slack: { handleSlackAction: handleSlackActionMock, }, }, }), })); import { slackPlugin } from "./channel.js"; describe("slackPlugin actions", () => { it("prefers session lookup for announce target routing", () => { expect(slackPlugin.meta.preferSessionLookupForAnnounceTarget).toBe(true); }); it("forwards read threadId to Slack action handler", async () => { handleSlackActionMock.mockResolvedValueOnce({ messages: [], hasMore: false }); const handleAction = slackPlugin.actions?.handleAction; expect(handleAction).toBeDefined(); await handleAction!({ action: "read", channel: "slack", accountId: "default", cfg: {}, params: { channelId: "C123", threadId: "1712345678.123456", }, }); expect(handleSlackActionMock).toHaveBeenCalledWith( expect.objectContaining({ action: "readMessages", channelId: "C123", threadId: "1712345678.123456", }), {}, undefined, ); }); }); describe("slackPlugin outbound", () => { const cfg = { channels: { slack: { botToken: "xoxb-test", appToken: "xapp-test", }, }, }; it("uses threadId as threadTs fallback for sendText", async () => { const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-text" }); const sendText = slackPlugin.outbound?.sendText; expect(sendText).toBeDefined(); const result = await sendText!({ cfg, to: "C123", text: "hello", accountId: "default", threadId: "1712345678.123456", deps: { sendSlack }, }); expect(sendSlack).toHaveBeenCalledWith( "C123", "hello", expect.objectContaining({ threadTs: "1712345678.123456", }), ); expect(result).toEqual({ channel: "slack", messageId: "m-text" }); }); it("prefers replyToId over threadId for sendMedia", async () => { const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-media" }); const sendMedia = slackPlugin.outbound?.sendMedia; expect(sendMedia).toBeDefined(); const result = await sendMedia!({ cfg, to: "C999", text: "caption", mediaUrl: "https://example.com/image.png", accountId: "default", replyToId: "1712000000.000001", threadId: "1712345678.123456", deps: { sendSlack }, }); expect(sendSlack).toHaveBeenCalledWith( "C999", "caption", expect.objectContaining({ mediaUrl: "https://example.com/image.png", threadTs: "1712000000.000001", }), ); expect(result).toEqual({ channel: "slack", messageId: "m-media" }); }); it("forwards mediaLocalRoots for sendMedia", async () => { const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-media-local" }); const sendMedia = slackPlugin.outbound?.sendMedia; expect(sendMedia).toBeDefined(); const mediaLocalRoots = ["/tmp/workspace"]; const result = await sendMedia!({ cfg, to: "C999", text: "caption", mediaUrl: "/tmp/workspace/image.png", mediaLocalRoots, accountId: "default", deps: { sendSlack }, }); expect(sendSlack).toHaveBeenCalledWith( "C999", "caption", expect.objectContaining({ mediaUrl: "/tmp/workspace/image.png", mediaLocalRoots, }), ); expect(result).toEqual({ channel: "slack", messageId: "m-media-local" }); }); }); describe("slackPlugin config", () => { it("treats HTTP mode accounts with bot token + signing secret as configured", async () => { const cfg: OpenClawConfig = { channels: { slack: { mode: "http", botToken: "xoxb-http", signingSecret: "secret-http", // pragma: allowlist secret }, }, }; const account = slackPlugin.config.resolveAccount(cfg, "default"); const configured = slackPlugin.config.isConfigured?.(account, cfg); const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({ account, cfg, runtime: undefined, }); expect(configured).toBe(true); expect(snapshot?.configured).toBe(true); }); it("keeps socket mode requiring app token", async () => { const cfg: OpenClawConfig = { channels: { slack: { mode: "socket", botToken: "xoxb-socket", }, }, }; const account = slackPlugin.config.resolveAccount(cfg, "default"); const configured = slackPlugin.config.isConfigured?.(account, cfg); const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({ account, cfg, runtime: undefined, }); expect(configured).toBe(false); expect(snapshot?.configured).toBe(false); }); it("does not mark partial configured-unavailable token status as configured", async () => { const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({ account: { accountId: "default", name: "Default", enabled: true, configured: false, botTokenStatus: "configured_unavailable", appTokenStatus: "missing", botTokenSource: "config", appTokenSource: "none", config: {}, } as never, cfg: {} as OpenClawConfig, runtime: undefined, }); expect(snapshot?.configured).toBe(false); expect(snapshot?.botTokenStatus).toBe("configured_unavailable"); expect(snapshot?.appTokenStatus).toBe("missing"); }); it("keeps HTTP mode signing-secret unavailable accounts configured in snapshots", async () => { const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({ account: { accountId: "default", name: "Default", enabled: true, configured: true, mode: "http", botTokenStatus: "available", signingSecretStatus: "configured_unavailable", // pragma: allowlist secret botTokenSource: "config", signingSecretSource: "config", // pragma: allowlist secret config: { mode: "http", botToken: "xoxb-http", signingSecret: { source: "env", provider: "default", id: "SLACK_SIGNING_SECRET" }, }, } as never, cfg: {} as OpenClawConfig, runtime: undefined, }); expect(snapshot?.configured).toBe(true); expect(snapshot?.botTokenStatus).toBe("available"); expect(snapshot?.signingSecretStatus).toBe("configured_unavailable"); }); });