| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
- import { CooldownManager, mergeBatchText } from "./cooldown.js";
- import type { RawWechatMessage } from "./types.js";
- function msg(overrides: Partial<RawWechatMessage> = {}): RawWechatMessage {
- return {
- msgId: overrides.msgId ?? `m-${Math.random().toString(36).slice(2, 8)}`,
- fromWxid: overrides.fromWxid ?? "wxid_alice",
- toWxid: overrides.toWxid ?? "wxid_bot",
- type: "text",
- content: overrides.content ?? "hello",
- timestamp: Date.now(),
- isGroup: false,
- ...overrides,
- };
- }
- describe("CooldownManager", () => {
- beforeEach(() => {
- vi.useFakeTimers();
- });
- afterEach(() => {
- vi.useRealTimers();
- });
- it("dispatches a single message after batchWindowMs", async () => {
- const flushed: RawWechatMessage[][] = [];
- const mgr = new CooldownManager({
- batchWindowMs: 500,
- replyCooldownMs: 0,
- onFlush: async (b) => {
- flushed.push(b);
- },
- });
- mgr.enqueue(msg({ content: "hi" }));
- expect(flushed).toHaveLength(0);
- await vi.advanceTimersByTimeAsync(500);
- expect(flushed).toHaveLength(1);
- expect(flushed[0]).toHaveLength(1);
- });
- it("merges rapid-fire messages from the same wxid into one batch", async () => {
- const flushed: RawWechatMessage[][] = [];
- const mgr = new CooldownManager({
- batchWindowMs: 500,
- replyCooldownMs: 0,
- onFlush: async (b) => {
- flushed.push(b);
- },
- });
- mgr.enqueue(msg({ content: "one" }));
- await vi.advanceTimersByTimeAsync(100);
- mgr.enqueue(msg({ content: "two" }));
- await vi.advanceTimersByTimeAsync(100);
- mgr.enqueue(msg({ content: "three" }));
- expect(flushed).toHaveLength(0);
- await vi.advanceTimersByTimeAsync(500);
- expect(flushed).toHaveLength(1);
- expect(flushed[0]).toHaveLength(3);
- expect(flushed[0].map((m) => m.content)).toEqual(["one", "two", "three"]);
- });
- it("keeps batches separated per wxid", async () => {
- const flushed: RawWechatMessage[][] = [];
- const mgr = new CooldownManager({
- batchWindowMs: 500,
- replyCooldownMs: 0,
- onFlush: async (b) => {
- flushed.push(b);
- },
- });
- mgr.enqueue(msg({ fromWxid: "wxid_alice", content: "a1" }));
- mgr.enqueue(msg({ fromWxid: "wxid_bob", content: "b1" }));
- mgr.enqueue(msg({ fromWxid: "wxid_alice", content: "a2" }));
- await vi.advanceTimersByTimeAsync(500);
- expect(flushed).toHaveLength(2);
- const alice = flushed.find((b) => b[0].fromWxid === "wxid_alice")!;
- const bob = flushed.find((b) => b[0].fromWxid === "wxid_bob")!;
- expect(alice.map((m) => m.content)).toEqual(["a1", "a2"]);
- expect(bob.map((m) => m.content)).toEqual(["b1"]);
- });
- it("delays flush until post-reply cooldown expires", async () => {
- const flushed: RawWechatMessage[][] = [];
- const mgr = new CooldownManager({
- batchWindowMs: 200,
- replyCooldownMs: 1000,
- onFlush: async (b) => {
- flushed.push(b);
- },
- });
- // Simulate a reply that just went out.
- mgr.noteReply("wxid_alice", Date.now());
- mgr.enqueue(msg({ fromWxid: "wxid_alice", content: "after reply" }));
- // Not yet — cooldown of 1s is still active.
- await vi.advanceTimersByTimeAsync(500);
- expect(flushed).toHaveLength(0);
- // Past the cooldown window, flush fires.
- await vi.advanceTimersByTimeAsync(600);
- expect(flushed).toHaveLength(1);
- expect(flushed[0][0].content).toBe("after reply");
- });
- it("stop() clears pending timers", async () => {
- const flushed: RawWechatMessage[][] = [];
- const mgr = new CooldownManager({
- batchWindowMs: 500,
- replyCooldownMs: 0,
- onFlush: async (b) => {
- flushed.push(b);
- },
- });
- mgr.enqueue(msg());
- mgr.stop();
- await vi.advanceTimersByTimeAsync(1000);
- expect(flushed).toHaveLength(0);
- });
- });
- describe("mergeBatchText", () => {
- it("joins non-empty contents with newlines", () => {
- const batch = [
- msg({ content: "hello" }),
- msg({ content: "" }),
- msg({ content: " " }),
- msg({ content: "world" }),
- ];
- expect(mergeBatchText(batch)).toBe("hello\nworld");
- });
- it("returns empty string for empty batch", () => {
- expect(mergeBatchText([])).toBe("");
- });
- });
|