import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MessageBatcher, messageBatchKey } from "../../src/feishu/messageBatcher.js"; describe("MessageBatcher", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("flushes a single message after the debounce period", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 25 }); await batcher.enqueue("chat-1", "sender-1", "hello"); expect(flushed).toEqual([]); await vi.advanceTimersByTimeAsync(24); expect(flushed).toEqual([]); await vi.advanceTimersByTimeAsync(1); expect(flushed).toEqual(["hello"]); }); it("merges two quick messages with a newline", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 25 }); await batcher.enqueue("chat-1", "sender-1", "first"); await batcher.enqueue("chat-1", "sender-1", "second"); await vi.advanceTimersByTimeAsync(25); expect(flushed).toEqual(["first\nsecond"]); }); it("flushes immediately when the max message count is reached", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 1000, maxMessages: 2 }); await batcher.enqueue("chat-1", "sender-1", "first"); expect(flushed).toEqual([]); await batcher.enqueue("chat-1", "sender-1", "second"); expect(flushed).toEqual(["first\nsecond"]); }); it("flushes immediately when the max character count is reached", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 1000, maxChars: 5 }); await batcher.enqueue("chat-1", "sender-1", "abc"); expect(flushed).toEqual([]); await batcher.enqueue("chat-1", "sender-1", "d"); expect(flushed).toEqual(["abc\nd"]); }); it("uses the extended debounce for a chunk near the split threshold", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 10, splitThreshold: 5, extendedDebounceMs: 50 }); await batcher.enqueue("chat-1", "sender-1", "12345"); await vi.advanceTimersByTimeAsync(49); expect(flushed).toEqual([]); await vi.advanceTimersByTimeAsync(1); expect(flushed).toEqual(["12345"]); }); it("keeps different chat/sender pairs in separate batches", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 25 }); await batcher.enqueue("chat-1", "sender-1", "one"); await batcher.enqueue("chat-1", "sender-2", "two"); await batcher.enqueue("chat-2", "sender-1", "three"); await vi.advanceTimersByTimeAsync(25); expect(flushed.sort()).toEqual(["one", "three", "two"]); }); it("flushAll flushes every pending batch", async () => { const flushed: string[] = []; const batcher = new MessageBatcher(async (text) => { flushed.push(text); }, { debounceMs: 1000 }); await batcher.enqueue("chat-1", "sender-1", "one"); await batcher.enqueue("chat-2", "sender-2", "two"); await batcher.flushAll(); expect(flushed.sort()).toEqual(["one", "two"]); }); it("builds keys from chat and sender open ids", () => { expect(messageBatchKey("chat-1", "ou-1")).toBe("chat-1:ou-1"); }); // Slash-command bypass is owned by trigger.ts, not MessageBatcher. Existing // trigger integration tests cover /new, /resume, /reset as immediate paths. });