import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { dispatchInboundMessage, withReplyDispatcher } from "./dispatch.js"; import type { ReplyDispatcher } from "./reply/reply-dispatcher.js"; import { buildTestCtx } from "./reply/test-ctx.js"; function createDispatcher(record: string[]): ReplyDispatcher { return { sendToolResult: () => true, sendBlockReply: () => true, sendFinalReply: () => true, getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }), markComplete: () => { record.push("markComplete"); }, waitForIdle: async () => { record.push("waitForIdle"); }, }; } describe("withReplyDispatcher", () => { it("always marks complete and waits for idle after success", async () => { const order: string[] = []; const dispatcher = createDispatcher(order); const result = await withReplyDispatcher({ dispatcher, run: async () => { order.push("run"); return "ok"; }, onSettled: () => { order.push("onSettled"); }, }); expect(result).toBe("ok"); expect(order).toEqual(["run", "markComplete", "waitForIdle", "onSettled"]); }); it("still drains dispatcher after run throws", async () => { const order: string[] = []; const dispatcher = createDispatcher(order); const onSettled = vi.fn(() => { order.push("onSettled"); }); await expect( withReplyDispatcher({ dispatcher, run: async () => { order.push("run"); throw new Error("boom"); }, onSettled, }), ).rejects.toThrow("boom"); expect(onSettled).toHaveBeenCalledTimes(1); expect(order).toEqual(["run", "markComplete", "waitForIdle", "onSettled"]); }); it("dispatchInboundMessage owns dispatcher lifecycle", async () => { const order: string[] = []; const dispatcher = { sendToolResult: () => true, sendBlockReply: () => true, sendFinalReply: () => { order.push("sendFinalReply"); return true; }, getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }), markComplete: () => { order.push("markComplete"); }, waitForIdle: async () => { order.push("waitForIdle"); }, } satisfies ReplyDispatcher; await dispatchInboundMessage({ ctx: buildTestCtx(), cfg: {} as OpenClawConfig, dispatcher, replyResolver: async () => ({ text: "ok" }), }); expect(order).toEqual(["sendFinalReply", "markComplete", "waitForIdle"]); }); });