// Slack helper module supports monitor helpers behavior. import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { vi } from "vitest"; import type { Mock } from "vitest"; import { clearSlackInboundDeliveryStateForTest } from "./monitor/inbound-delivery-state.js"; type SlackHandler = (args: unknown) => Promise; type SlackMiddleware = (args: { next: () => Promise } & Record) => unknown; type SlackProviderMonitor = (params: { botToken: string; appToken: string; abortSignal: AbortSignal; config?: Record; channelRuntime?: ChannelRuntimeSurface; runtime?: RuntimeEnv; }) => Promise; type SlackTestState = { config: Record; appStartMock: Mock<(...args: unknown[]) => Promise>; appStopMock: Mock<(...args: unknown[]) => Promise>; sendMock: Mock<(...args: unknown[]) => Promise>; replyMock: Mock<(...args: unknown[]) => unknown>; updateLastRouteMock: Mock<(...args: unknown[]) => unknown>; reactMock: Mock<(...args: unknown[]) => unknown>; reactionAddMock: Mock<(...args: unknown[]) => unknown>; reactionRemoveMock: Mock<(...args: unknown[]) => unknown>; readAllowFromStoreMock: Mock<(...args: unknown[]) => Promise>; upsertPairingRequestMock: Mock<(...args: unknown[]) => Promise>; resolveSlackUserAllowlistMock: Mock< (params: { entries: string[] }) => Promise> >; }; const slackTestState: SlackTestState = vi.hoisted(() => ({ config: {} as Record, appStartMock: vi.fn(), appStopMock: vi.fn(), sendMock: vi.fn(), replyMock: vi.fn(), updateLastRouteMock: vi.fn(), reactMock: vi.fn(), reactionAddMock: vi.fn(), reactionRemoveMock: vi.fn(), readAllowFromStoreMock: vi.fn(), upsertPairingRequestMock: vi.fn(), resolveSlackUserAllowlistMock: vi.fn(), })); export const getSlackTestState = (): SlackTestState => slackTestState; type SlackClient = { auth: { test: Mock<(...args: unknown[]) => Promise>> }; conversations: { info: Mock<(...args: unknown[]) => Promise>>; replies: Mock<(...args: unknown[]) => Promise>>; history: Mock<(...args: unknown[]) => Promise>>; }; users: { info: Mock<(...args: unknown[]) => Promise<{ user: { profile: { display_name: string } } }>>; }; assistant: { threads: { setStatus: Mock<(...args: unknown[]) => Promise<{ ok: boolean }>>; }; }; reactions: { add: (...args: unknown[]) => unknown; remove: (...args: unknown[]) => unknown; }; }; export const getSlackHandlers = () => ensureSlackTestRuntime().handlers; export const getSlackClient = () => ensureSlackTestRuntime().client; function ensureSlackTestRuntime(): { handlers: Map; client: SlackClient; } { const globalState = globalThis as { __slackHandlers?: Map; __slackClient?: SlackClient; }; if (!globalState["__slackHandlers"]) { globalState["__slackHandlers"] = new Map(); } if (!globalState["__slackClient"]) { globalState["__slackClient"] = { auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user", bot_id: "bot-id" }) }, conversations: { info: vi.fn().mockResolvedValue({ channel: { name: "dm", is_im: true }, }), replies: vi.fn().mockResolvedValue({ messages: [] }), history: vi.fn().mockResolvedValue({ messages: [] }), }, users: { info: vi.fn().mockResolvedValue({ user: { profile: { display_name: "Ada" } }, }), }, assistant: { threads: { setStatus: vi.fn().mockResolvedValue({ ok: true }), }, }, reactions: { add: (...args: unknown[]) => { slackTestState.reactionAddMock(...args); return slackTestState.reactMock(...args); }, remove: (...args: unknown[]) => { slackTestState.reactionRemoveMock(...args); return slackTestState.reactMock(...args); }, }, }; } return { handlers: globalState["__slackHandlers"], client: globalState["__slackClient"], }; } export const flush = () => new Promise((resolve) => { setTimeout(resolve, 0); }); async function waitForSlackEvent(name: string) { for (let i = 0; i < 10; i += 1) { if (getSlackHandlers()?.has(name)) { return; } await flush(); } } export function startSlackMonitor( monitorSlackProvider: SlackProviderMonitor, opts?: { botToken?: string; appToken?: string; channelRuntime?: ChannelRuntimeSurface; runtime?: RuntimeEnv; }, ) { const controller = new AbortController(); const run = monitorSlackProvider({ botToken: opts?.botToken ?? "bot-token", appToken: opts?.appToken ?? "app-token", abortSignal: controller.signal, config: slackTestState.config, channelRuntime: opts?.channelRuntime, runtime: opts?.runtime, }); return { controller, run }; } export async function getSlackHandlerOrThrow(name: string) { await waitForSlackEvent(name); const handler = getSlackHandlers()?.get(name); if (!handler) { throw new Error(`Slack ${name} handler not registered`); } return handler; } export async function stopSlackMonitor(params: { controller: AbortController; run: Promise; }) { await flush(); params.controller.abort(); await params.run; } async function runSlackEventOnce( monitorSlackProvider: SlackProviderMonitor, name: string, args: unknown, opts?: { botToken?: string; appToken?: string }, ) { const { controller, run } = startSlackMonitor(monitorSlackProvider, opts); const handler = await getSlackHandlerOrThrow(name); await handler(args); await stopSlackMonitor({ controller, run }); } export async function runSlackMessageOnce( monitorSlackProvider: SlackProviderMonitor, args: unknown, opts?: { botToken?: string; appToken?: string }, ) { await runSlackEventOnce(monitorSlackProvider, "message", args, opts); } export const defaultSlackTestConfig = () => ({ messages: { responsePrefix: "PFX", ackReaction: "👀", ackReactionScope: "group-mentions", }, channels: { slack: { dm: { enabled: true, policy: "open", allowFrom: ["*"] }, groupPolicy: "open", }, }, }); export function resetSlackTestState(config: Record = defaultSlackTestConfig()) { clearSlackInboundDeliveryStateForTest(); slackTestState.config = config; slackTestState.appStartMock.mockReset().mockResolvedValue(undefined); slackTestState.appStopMock.mockReset().mockResolvedValue(undefined); slackTestState.sendMock.mockReset().mockResolvedValue(undefined); slackTestState.replyMock.mockReset(); slackTestState.updateLastRouteMock.mockReset(); slackTestState.reactMock.mockReset(); slackTestState.reactionAddMock.mockReset(); slackTestState.reactionRemoveMock.mockReset(); slackTestState.readAllowFromStoreMock.mockReset().mockResolvedValue([]); slackTestState.upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true, }); slackTestState.resolveSlackUserAllowlistMock .mockReset() .mockImplementation(async ({ entries }) => entries.map((input) => ({ input, resolved: false })), ); const client = getSlackClient(); client.auth.test.mockReset().mockResolvedValue({ user_id: "bot-user", bot_id: "bot-id" }); client.conversations.info.mockReset().mockResolvedValue({ channel: { name: "dm", is_im: true }, }); client.conversations.replies.mockReset().mockResolvedValue({ messages: [] }); client.conversations.history.mockReset().mockResolvedValue({ messages: [] }); client.users.info.mockReset().mockResolvedValue({ user: { profile: { display_name: "Ada" } }, }); client.assistant.threads.setStatus.mockReset().mockResolvedValue({ ok: true }); getSlackHandlers()?.clear(); } vi.mock("./monitor/config.runtime.js", async () => { const actual = await vi.importActual( "./monitor/config.runtime.js", ); return { ...actual, loadConfig: () => slackTestState.config, readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"), updateLastRoute: (...args: unknown[]) => slackTestState.updateLastRouteMock(...args), }; }); vi.mock("./monitor/reply.runtime.js", async () => { const actual = await vi.importActual( "./monitor/reply.runtime.js", ); type DispatchParams = Parameters[0]; type BufferedDispatchParams = Parameters< typeof actual.dispatchReplyWithBufferedBlockDispatcher >[0]; type ReplyResolver = NonNullable; const replyResolver: ReplyResolver = (...args) => slackTestState.replyMock(...args) as ReturnType; return { ...actual, dispatchInboundMessage: (params: Parameters[0]) => actual.dispatchInboundMessage({ ...params, replyResolver, }), dispatchReplyWithBufferedBlockDispatcher: (params: BufferedDispatchParams) => actual.dispatchReplyWithBufferedBlockDispatcher({ ...params, replyResolver, }), }; }); vi.mock("./resolve-channels.js", () => ({ resolveSlackChannelAllowlist: async ({ entries }: { entries: string[] }) => entries.map((input) => ({ input, resolved: false })), })); vi.mock("./resolve-users.js", () => ({ resolveSlackUserAllowlist: (params: { entries: string[] }) => slackTestState.resolveSlackUserAllowlistMock(params), })); vi.mock("./monitor/send.runtime.js", () => { return { sendMessageSlack: (...args: unknown[]) => slackTestState.sendMock(...args), }; }); vi.mock("./monitor/conversation.runtime.js", async () => { const actual = await vi.importActual( "./monitor/conversation.runtime.js", ); return { ...actual, readChannelAllowFromStore: (...args: unknown[]) => slackTestState.readAllowFromStoreMock(...args), recordInboundSession: vi.fn().mockResolvedValue(undefined), upsertChannelPairingRequest: (...args: unknown[]) => slackTestState.upsertPairingRequestMock(...args), }; }); vi.mock("@slack/bolt", () => { const { handlers, client: slackClient } = ensureSlackTestRuntime(); class App { client = slackClient; receiver: unknown; middlewares: SlackMiddleware[] = []; constructor(args?: { receiver?: unknown }) { this.receiver = args?.receiver; } use(middleware: SlackMiddleware) { this.middlewares.push(middleware); } event(name: string, handler: SlackHandler) { handlers.set(name, async (args: unknown) => { const eventArgs = args && typeof args === "object" && !Array.isArray(args) ? (args as Record) : {}; const run = async (index: number): Promise => { const middleware = this.middlewares[index]; if (!middleware) { await handler(args); return; } await middleware({ ...eventArgs, next: () => run(index + 1), }); }; await run(0); }); } command() { /* no-op */ } start = (...args: unknown[]) => slackTestState.appStartMock(...args); stop = (...args: unknown[]) => slackTestState.appStopMock(...args); } class HTTPReceiver { requestListener = vi.fn(); } class SocketModeReceiver { client = { ...slackClient, on: vi.fn(), off: vi.fn(), }; } return { App, HTTPReceiver, SocketModeReceiver, default: { App, HTTPReceiver, SocketModeReceiver }, }; });