From fcd1073b727a11e8f7863dabe3fb5b3d4022bdd9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 12 May 2026 16:41:28 +0100 Subject: [PATCH] test: dedupe auto-reply dispatch mock calls --- .../reply/dispatch-from-config.test.ts | 103 +++++++++--------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index ec1badd19bf..1e554b7bfa8 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -702,10 +702,34 @@ function createMockAcpSessionManager() { }; } +function firstMockArg(mockFn: ReturnType, label: string, index = 0): unknown { + const call = mockFn.mock.calls.at(index); + if (!call) { + throw new Error(`expected ${label} call #${index + 1}`); + } + return call.at(0); +} + function firstToolResultPayload(dispatcher: ReplyDispatcher): ReplyPayload | undefined { - return (dispatcher.sendToolResult as ReturnType).mock.calls.at(0)?.[0] as - | ReplyPayload - | undefined; + return firstMockArg( + dispatcher.sendToolResult as ReturnType, + "tool result", + ) as ReplyPayload; +} + +function firstFinalReplyPayload(dispatcher: ReplyDispatcher): ReplyPayload | undefined { + return firstMockArg( + dispatcher.sendFinalReply as ReturnType, + "final reply", + ) as ReplyPayload; +} + +function firstRouteReplyCall(): Record { + const call = firstMockArg(mocks.routeReply, "route reply"); + if (!call || typeof call !== "object") { + throw new Error("expected route reply params"); + } + return call as Record; } function requireToolResultHandler( @@ -916,7 +940,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { accountId?: unknown; channel?: unknown; @@ -963,7 +987,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { accountId?: unknown; channel?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("telegram"); @@ -1012,7 +1036,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { accountId?: unknown; channel?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("discord"); @@ -1108,7 +1132,7 @@ describe("dispatchReplyFromConfig", () => { const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload; await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { channel?: unknown; threadId?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("discord"); @@ -1149,7 +1173,7 @@ describe("dispatchReplyFromConfig", () => { const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload; await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { channel?: string; to?: string; threadId?: string | number } | undefined; expect(routeCall?.channel).toBe("mattermost"); @@ -1212,9 +1236,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as - | { channel?: unknown; to?: unknown } - | undefined; + const routeCall = firstRouteReplyCall() as { channel?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("telegram"); expect(routeCall?.to).toBe("telegram:999"); }); @@ -1235,9 +1257,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as - | { channel?: unknown; to?: unknown } - | undefined; + const routeCall = firstRouteReplyCall() as { channel?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("feishu"); expect(routeCall?.to).toBe("ou_feishu_direct_123"); }); @@ -1305,7 +1325,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { channel?: unknown; policyConversationType?: unknown; to?: unknown } | undefined; expect(routeCall?.channel).toBe("imessage"); @@ -1348,7 +1368,7 @@ describe("dispatchReplyFromConfig", () => { expect(dispatcher.sendToolResult).not.toHaveBeenCalled(); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); expect(mocks.routeReply).toHaveBeenCalledTimes(1); - const routed = mocks.routeReply.mock.calls.at(0)?.[0] as { payload?: ReplyPayload } | undefined; + const routed = firstRouteReplyCall() as { payload?: ReplyPayload } | undefined; expect(routed?.payload?.mediaUrls).toEqual(["https://example.com/tts-routed.opus"]); expect(routed?.payload?.text).toBeUndefined(); }); @@ -2068,9 +2088,7 @@ describe("dispatchReplyFromConfig", () => { const streamedText = blockCalls.map((call) => (call[0] as ReplyPayload).text ?? "").join(""); expect(streamedText).toContain("hello"); expect(streamedText).toContain("world"); - const finalPayload = (dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as - | ReplyPayload - | undefined; + const finalPayload = firstFinalReplyPayload(dispatcher); expect(finalPayload?.text).toBe("hello world"); }); @@ -2559,9 +2577,7 @@ describe("dispatchReplyFromConfig", () => { } } expect(blockTexts).toEqual(["What do you want to work on?"]); - const finalPayload = (dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as - | ReplyPayload - | undefined; + const finalPayload = firstFinalReplyPayload(dispatcher); expect(finalPayload?.text).toBe("What do you want to work on?"); }); @@ -2644,9 +2660,7 @@ describe("dispatchReplyFromConfig", () => { const normalizerOptions = replyMediaPathMocks.createReplyMediaPathNormalizer.mock .calls[0]?.[0] as { messageProvider?: unknown } | undefined; expect(normalizerOptions?.messageProvider).toBe("feishu"); - const finalPayload = (dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as - | ReplyPayload - | undefined; + const finalPayload = firstFinalReplyPayload(dispatcher); expect(finalPayload?.mediaUrl).toBe("/tmp/openclaw-media/normalized-tts.ogg"); expect(finalPayload?.mediaUrls).toStrictEqual(["/tmp/openclaw-media/normalized-tts.ogg"]); expect(finalPayload?.audioAsVoice).toBe(true); @@ -2800,9 +2814,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(firstToolResultPayload(dispatcher)?.text).toBe("Approval required."); - expect( - ((dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as ReplyPayload | undefined)?.text, - ).toBe("done"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("done"); }); it("suppresses local discord exec approval tool prompts when the native runtime is active", async () => { @@ -2850,10 +2862,7 @@ describe("dispatchReplyFromConfig", () => { await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); expect(dispatcher.sendToolResult).not.toHaveBeenCalled(); - expect( - ((dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as ReplyPayload | undefined) - ?.text, - ).toBe("done"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("done"); } finally { await reporter.stop(); } @@ -3005,9 +3014,7 @@ describe("dispatchReplyFromConfig", () => { expect(internalHookEvent?.action).toBe("received"); expect(internalHookEvent?.sessionKey).toBe("agent:main:hook-test"); expect(replyResolver).toHaveBeenCalledTimes(1); - expect( - ((dispatcher.sendFinalReply as Mock).mock.calls.at(0)?.[0] as ReplyPayload | undefined)?.text, - ).toBe("core reply"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("core reply"); }); it("emits internal message:received hook when a session key is available", async () => { @@ -4180,7 +4187,7 @@ describe("before_dispatch hook", () => { expect(beforeDispatchCall?.[1]?.channelId).toBe("telegram"); expect(beforeDispatchCall?.[1]?.senderId).toBe("signal:user:alice"); expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); - const routeCall = mocks.routeReply.mock.calls.at(0)?.[0] as + const routeCall = firstRouteReplyCall() as | { channel?: unknown; payload?: ReplyPayload; to?: unknown } | undefined; expect(routeCall?.channel).toBe("telegram"); @@ -4839,9 +4846,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("visible direct reply"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("visible direct reply"); }); it("uses harness defaults for direct source delivery when config is unset", async () => { @@ -4901,9 +4906,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("visible fallback"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("visible fallback"); }); it("falls back to automatic group/channel delivery when group tools remove the message tool", async () => { @@ -4937,9 +4940,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("group policy fallback"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("group policy fallback"); }); it("falls back when a channel precomputed message-tool-only delivery but the message tool is unavailable", async () => { @@ -4965,9 +4966,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("requested fallback"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("requested fallback"); }); it("keeps native command replies visible in group/channel turns", async () => { @@ -4994,9 +4993,7 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("status reply"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("status reply"); }); it("allows config to keep group/channel source delivery automatic", async () => { @@ -5020,8 +5017,6 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () => expect(replyResolver).toHaveBeenCalledTimes(1); expect(result.queuedFinal).toBe(true); - expect( - (dispatcher.sendFinalReply as ReturnType).mock.calls.at(0)?.[0]?.text, - ).toBe("final reply"); + expect(firstFinalReplyPayload(dispatcher)?.text).toBe("final reply"); }); });