diff --git a/src/auto-reply/reply/typing.test.ts b/src/auto-reply/reply/typing.test.ts index 7ea4d2330..4026eec13 100644 --- a/src/auto-reply/reply/typing.test.ts +++ b/src/auto-reply/reply/typing.test.ts @@ -30,4 +30,25 @@ describe("typing controller", () => { vi.advanceTimersByTime(2_000); expect(onReplyStart).toHaveBeenCalledTimes(4); }); + + it("keeps typing until both idle and run completion are set", async () => { + vi.useFakeTimers(); + const onReplyStart = vi.fn(async () => {}); + const typing = createTypingController({ + onReplyStart, + typingIntervalSeconds: 1, + typingTtlMs: 30_000, + }); + + await typing.startTypingLoop(); + expect(onReplyStart).toHaveBeenCalledTimes(1); + + typing.markDispatchIdle(); + vi.advanceTimersByTime(2_000); + expect(onReplyStart).toHaveBeenCalledTimes(3); + + typing.markRunComplete(); + vi.advanceTimersByTime(2_000); + expect(onReplyStart).toHaveBeenCalledTimes(3); + }); }); diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 25cd46ea2..3b3525cbd 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -244,6 +244,63 @@ describe("partial reply gating", () => { }); }); +describe("typing controller idle", () => { + it("marks dispatch idle after replies flush", async () => { + const markDispatchIdle = vi.fn(); + const typingMock = { + onReplyStart: vi.fn(async () => {}), + startTypingLoop: vi.fn(async () => {}), + startTypingOnText: vi.fn(async () => {}), + refreshTypingTtl: vi.fn(), + markRunComplete: vi.fn(), + markDispatchIdle, + cleanup: vi.fn(), + }; + const reply = vi.fn().mockResolvedValue(undefined); + const sendComposing = vi.fn().mockResolvedValue(undefined); + const sendMedia = vi.fn().mockResolvedValue(undefined); + + const replyResolver = vi.fn().mockImplementation(async (_ctx, opts) => { + opts?.onTypingController?.(typingMock); + return { text: "final reply" }; + }); + + const mockConfig: ClawdbotConfig = { + whatsapp: { + allowFrom: ["*"], + }, + }; + + setLoadConfigMock(mockConfig); + + await monitorWebProvider( + false, + async ({ onMessage }) => { + await onMessage({ + id: "m1", + from: "+1000", + conversationId: "+1000", + to: "+2000", + body: "hello", + timestamp: Date.now(), + chatType: "direct", + chatId: "direct:+1000", + sendComposing, + reply, + sendMedia, + }); + return { close: vi.fn().mockResolvedValue(undefined) }; + }, + false, + replyResolver, + ); + + resetLoadConfigMock(); + + expect(markDispatchIdle).toHaveBeenCalled(); + }); +}); + describe("web auto-reply", () => { beforeEach(() => { vi.clearAllMocks();