import { beforeEach, describe, expect, it, vi } from "vitest"; import { createSlackMessageHandler } from "./message-handler.js"; const enqueueMock = vi.fn(async (_entry: unknown) => {}); const flushKeyMock = vi.fn(async (_key: string) => {}); const resolveThreadTsMock = vi.fn(async ({ message }: { message: Record }) => ({ ...message, })); vi.mock("../../auto-reply/inbound-debounce.js", () => ({ resolveInboundDebounceMs: () => 10, createInboundDebouncer: () => ({ enqueue: (entry: unknown) => enqueueMock(entry), flushKey: (key: string) => flushKeyMock(key), }), })); vi.mock("./thread-resolution.js", () => ({ createSlackThreadTsResolver: () => ({ resolve: (entry: { message: Record }) => resolveThreadTsMock(entry), }), })); function createContext(overrides?: { markMessageSeen?: (channel: string | undefined, ts: string | undefined) => boolean; }) { return { cfg: {}, accountId: "default", app: { client: {}, }, runtime: {}, markMessageSeen: (channel: string | undefined, ts: string | undefined) => overrides?.markMessageSeen?.(channel, ts) ?? false, } as Parameters[0]["ctx"]; } function createHandlerWithTracker(overrides?: { markMessageSeen?: (channel: string | undefined, ts: string | undefined) => boolean; }) { const trackEvent = vi.fn(); const handler = createSlackMessageHandler({ ctx: createContext(overrides), account: { accountId: "default" } as Parameters[0]["account"], trackEvent, }); return { handler, trackEvent }; } async function handleDirectMessage( handler: ReturnType["handler"], ) { await handler( { type: "message", channel: "D1", ts: "123.456", text: "hello", } as never, { source: "message" }, ); } describe("createSlackMessageHandler", () => { beforeEach(() => { enqueueMock.mockClear(); flushKeyMock.mockClear(); resolveThreadTsMock.mockClear(); }); it("does not track invalid non-message events from the message stream", async () => { const trackEvent = vi.fn(); const handler = createSlackMessageHandler({ ctx: createContext(), account: { accountId: "default" } as Parameters< typeof createSlackMessageHandler >[0]["account"], trackEvent, }); await handler( { type: "reaction_added", channel: "D1", ts: "123.456", } as never, { source: "message" }, ); expect(trackEvent).not.toHaveBeenCalled(); expect(resolveThreadTsMock).not.toHaveBeenCalled(); expect(enqueueMock).not.toHaveBeenCalled(); }); it("does not track duplicate messages that are already seen", async () => { const { handler, trackEvent } = createHandlerWithTracker({ markMessageSeen: () => true }); await handleDirectMessage(handler); expect(trackEvent).not.toHaveBeenCalled(); expect(resolveThreadTsMock).not.toHaveBeenCalled(); expect(enqueueMock).not.toHaveBeenCalled(); }); it("tracks accepted non-duplicate messages", async () => { const { handler, trackEvent } = createHandlerWithTracker(); await handleDirectMessage(handler); expect(trackEvent).toHaveBeenCalledTimes(1); expect(resolveThreadTsMock).toHaveBeenCalledTimes(1); expect(enqueueMock).toHaveBeenCalledTimes(1); }); it("flushes pending top-level buffered keys before immediate non-debounce follow-ups", async () => { const handler = createSlackMessageHandler({ ctx: createContext(), account: { accountId: "default" } as Parameters< typeof createSlackMessageHandler >[0]["account"], }); await handler( { type: "message", channel: "C111", user: "U111", ts: "1709000000.000100", text: "first buffered text", } as never, { source: "message" }, ); await handler( { type: "message", subtype: "file_share", channel: "C111", user: "U111", ts: "1709000000.000200", text: "file follows", files: [{ id: "F1" }], } as never, { source: "message" }, ); expect(flushKeyMock).toHaveBeenCalledWith("slack:default:C111:1709000000.000100:U111"); }); });