import { beforeEach, describe, expect, it, vi } from "vitest"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorSlackProvider } from "./monitor.js"; const sendMock = vi.fn(); const replyMock = vi.fn(); const updateLastRouteMock = vi.fn(); const reactMock = vi.fn(); let config: Record = {}; const readAllowFromStoreMock = vi.fn(); const upsertPairingRequestMock = vi.fn(); const getSlackHandlers = () => ( globalThis as { __slackHandlers?: Map Promise>; } ).__slackHandlers; const getSlackClient = () => (globalThis as { __slackClient?: Record }).__slackClient; vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => config, }; }); vi.mock("../auto-reply/reply.js", () => ({ getReplyFromConfig: (...args: unknown[]) => replyMock(...args), })); vi.mock("./resolve-channels.js", () => ({ resolveSlackChannelAllowlist: async ({ entries }: { entries: string[] }) => entries.map((input) => ({ input, resolved: false })), })); vi.mock("./resolve-users.js", () => ({ resolveSlackUserAllowlist: async ({ entries }: { entries: string[] }) => entries.map((input) => ({ input, resolved: false })), })); vi.mock("./send.js", () => ({ sendMessageSlack: (...args: unknown[]) => sendMock(...args), })); vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), })); vi.mock("../config/sessions.js", () => ({ resolveStorePath: vi.fn(() => "/tmp/moltbot-sessions.json"), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), resolveSessionKey: vi.fn(), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), })); vi.mock("@slack/bolt", () => { const handlers = new Map Promise>(); (globalThis as { __slackHandlers?: typeof handlers }).__slackHandlers = handlers; const client = { auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) }, conversations: { info: vi.fn().mockResolvedValue({ channel: { name: "general", is_channel: 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[]) => reactMock(...args), }, }; (globalThis as { __slackClient?: typeof client }).__slackClient = client; class App { client = client; event(name: string, handler: (args: unknown) => Promise) { handlers.set(name, handler); } command() { /* no-op */ } start = vi.fn().mockResolvedValue(undefined); stop = vi.fn().mockResolvedValue(undefined); } class HTTPReceiver { requestListener = vi.fn(); } return { App, HTTPReceiver, default: { App, HTTPReceiver } }; }); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); async function waitForEvent(name: string) { for (let i = 0; i < 10; i += 1) { if (getSlackHandlers()?.has(name)) return; await flush(); } } beforeEach(() => { resetInboundDedupe(); getSlackHandlers()?.clear(); config = { messages: { responsePrefix: "PFX" }, channels: { slack: { dm: { enabled: true, policy: "open", allowFrom: ["*"] }, groupPolicy: "open", channels: { C1: { allow: true, requireMention: false } }, }, }, }; sendMock.mockReset().mockResolvedValue(undefined); replyMock.mockReset(); updateLastRouteMock.mockReset(); reactMock.mockReset(); readAllowFromStoreMock.mockReset().mockResolvedValue([]); upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); }); describe("monitorSlackProvider threading", () => { it("recovers missing thread_ts when parent_user_id is present", async () => { replyMock.mockResolvedValue({ text: "thread reply" }); const client = getSlackClient(); if (!client) throw new Error("Slack client not registered"); const conversations = client.conversations as { history: ReturnType; }; conversations.history.mockResolvedValueOnce({ messages: [{ ts: "456", thread_ts: "111.222" }], }); const controller = new AbortController(); const run = monitorSlackProvider({ botToken: "bot-token", appToken: "app-token", abortSignal: controller.signal, }); await waitForEvent("message"); const handler = getSlackHandlers()?.get("message"); if (!handler) throw new Error("Slack message handler not registered"); await handler({ event: { type: "message", user: "U1", text: "hello", ts: "456", parent_user_id: "U2", channel: "C1", channel_type: "channel", }, }); await flush(); controller.abort(); await run; expect(sendMock).toHaveBeenCalledTimes(1); expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: "111.222" }); }); });