forked from EduCraft/curriculum-project-hub
ad65d93b2e
- Create MessageBatcher: debounce 600ms, adaptive delay for long chunks, max 8 messages/4000 chars before immediate flush, per (chatId, sender) key - Integrate into trigger: text messages enqueued, slash commands and file/image bypass batching, locked projects respond immediately - Add unit tests (8) for batcher lifecycle and edge cases
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
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.
|
|
});
|