mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 04:57:48 +00:00
test: dedupe auto-reply dispatch mock calls
This commit is contained in:
@@ -702,10 +702,34 @@ function createMockAcpSessionManager() {
|
||||
};
|
||||
}
|
||||
|
||||
function firstMockArg(mockFn: ReturnType<typeof vi.fn>, 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<typeof vi.fn>).mock.calls.at(0)?.[0] as
|
||||
| ReplyPayload
|
||||
| undefined;
|
||||
return firstMockArg(
|
||||
dispatcher.sendToolResult as ReturnType<typeof vi.fn>,
|
||||
"tool result",
|
||||
) as ReplyPayload;
|
||||
}
|
||||
|
||||
function firstFinalReplyPayload(dispatcher: ReplyDispatcher): ReplyPayload | undefined {
|
||||
return firstMockArg(
|
||||
dispatcher.sendFinalReply as ReturnType<typeof vi.fn>,
|
||||
"final reply",
|
||||
) as ReplyPayload;
|
||||
}
|
||||
|
||||
function firstRouteReplyCall(): Record<string, unknown> {
|
||||
const call = firstMockArg(mocks.routeReply, "route reply");
|
||||
if (!call || typeof call !== "object") {
|
||||
throw new Error("expected route reply params");
|
||||
}
|
||||
return call as Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mock.calls.at(0)?.[0]?.text,
|
||||
).toBe("final reply");
|
||||
expect(firstFinalReplyPayload(dispatcher)?.text).toBe("final reply");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user