





















@@ -2,6 +2,14 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
2233let capturedDispatchParams: unknown;
445+type CapturedReplyPayload = {
6+text?: string;
7+isReasoning?: boolean;
8+isCompactionNotice?: boolean;
9+mediaUrl?: string;
10+mediaUrls?: string[];
11+};
12+513const { dispatchReplyWithBufferedBlockDispatcherMock } = vi.hoisted(() => ({
614dispatchReplyWithBufferedBlockDispatcherMock: vi.fn(async (params: { ctx: unknown }) => {
715capturedDispatchParams = params;
@@ -36,10 +44,20 @@ vi.mock("./runtime-api.js", () => ({
3644},
3745resolveInboundLastRouteSessionKey: (params: { sessionKey: string }) => params.sessionKey,
3846resolveMarkdownTableMode: () => undefined,
39-resolveSendableOutboundReplyParts: (payload: { text?: string }) => ({
40-text: payload.text ?? "",
41-hasMedia: false,
42-}),
47+resolveSendableOutboundReplyParts: (payload: {
48+text?: string;
49+mediaUrls?: string[];
50+mediaUrl?: string;
51+}) => {
52+const urls = [
53+ ...(Array.isArray(payload.mediaUrls) ? payload.mediaUrls : []),
54+ ...(payload.mediaUrl ? [payload.mediaUrl] : []),
55+];
56+return {
57+text: payload.text ?? "",
58+hasMedia: urls.length > 0,
59+};
60+},
4361resolveTextChunkLimit: () => 4000,
4462shouldLogVerbose: () => false,
4563toLocationContext: () => ({}),
@@ -91,7 +109,7 @@ function getCapturedDeliver() {
91109capturedDispatchParams as {
92110dispatcherOptions?: {
93111deliver?: (
94-payload: { text?: string; isReasoning?: boolean; isCompactionNotice?: boolean },
112+payload: CapturedReplyPayload,
95113info: { kind: "tool" | "block" | "final" },
96114) => Promise<void>;
97115};
@@ -360,7 +378,7 @@ describe("whatsapp inbound dispatch", () => {
360378expect(groupHistories.get("whatsapp:default:group:123@g.us") ?? []).toHaveLength(0);
361379});
362380363-it("delivers block and final WhatsApp payloads, but suppresses tool payloads", async () => {
381+it("delivers block and final WhatsApp payloads; suppresses text-only tool payloads but delivers tool media", async () => {
364382const deliverReply = vi.fn(async () => undefined);
365383const rememberSentText = vi.fn();
366384@@ -376,10 +394,27 @@ describe("whatsapp inbound dispatch", () => {
376394expect(deliverReply).not.toHaveBeenCalled();
377395expect(rememberSentText).not.toHaveBeenCalled();
378396397+await deliver?.(
398+{ text: "tool image", mediaUrls: ["/tmp/generated.jpg"] },
399+{
400+kind: "tool",
401+},
402+);
403+expect(deliverReply).toHaveBeenCalledTimes(1);
404+expect(rememberSentText).toHaveBeenCalledTimes(1);
405+expect(deliverReply).toHaveBeenLastCalledWith(
406+expect.objectContaining({
407+replyResult: expect.objectContaining({
408+mediaUrls: ["/tmp/generated.jpg"],
409+text: undefined,
410+}),
411+}),
412+);
413+379414await deliver?.({ text: "block payload" }, { kind: "block" });
380415await deliver?.({ text: "final payload" }, { kind: "final" });
381-expect(deliverReply).toHaveBeenCalledTimes(2);
382-expect(rememberSentText).toHaveBeenCalledTimes(2);
416+expect(deliverReply).toHaveBeenCalledTimes(3);
417+expect(rememberSentText).toHaveBeenCalledTimes(3);
383418});
384419385420it("suppresses reasoning and compaction payloads before WhatsApp delivery", async () => {
@@ -473,6 +508,65 @@ describe("whatsapp inbound dispatch", () => {
473508expect(rememberSentText).toHaveBeenCalledTimes(1);
474509});
475510511+it("returns true for tool-only media turns after delivering media", async () => {
512+const deliverReply = vi.fn(async () => undefined);
513+const rememberSentText = vi.fn();
514+dispatchReplyWithBufferedBlockDispatcherMock.mockImplementationOnce(
515+async (params: {
516+ctx: unknown;
517+dispatcherOptions?: {
518+deliver?: (
519+payload: CapturedReplyPayload,
520+info: { kind: "tool" | "block" | "final" },
521+) => Promise<void>;
522+};
523+}) => {
524+capturedDispatchParams = params;
525+await params.dispatcherOptions?.deliver?.(
526+{ text: "tool image", mediaUrls: ["/tmp/generated.jpg"] },
527+{ kind: "tool" },
528+);
529+return { queuedFinal: false, counts: { tool: 1, block: 0, final: 0 } };
530+},
531+);
532+533+await expect(
534+dispatchWhatsAppBufferedReply({
535+cfg: { channels: { whatsapp: { blockStreaming: true } } } as never,
536+connectionId: "conn",
537+context: { Body: "hi" },
538+conversationId: "+1000",
539+ deliverReply,
540+groupHistories: new Map(),
541+groupHistoryKey: "+1000",
542+maxMediaBytes: 1,
543+msg: makeMsg(),
544+ rememberSentText,
545+replyLogger: {
546+info: () => {},
547+warn: () => {},
548+error: () => {},
549+debug: () => {},
550+} as never,
551+replyPipeline: {},
552+replyResolver: (async () => undefined) as never,
553+route: makeRoute(),
554+shouldClearGroupHistory: false,
555+}),
556+).resolves.toBe(true);
557+558+expect(deliverReply).toHaveBeenCalledTimes(1);
559+expect(deliverReply).toHaveBeenCalledWith(
560+expect.objectContaining({
561+replyResult: expect.objectContaining({
562+mediaUrls: ["/tmp/generated.jpg"],
563+text: undefined,
564+}),
565+}),
566+);
567+expect(rememberSentText).toHaveBeenCalledWith(undefined, expect.any(Object));
568+});
569+476570it("passes sendComposing through as the reply typing callback", async () => {
477571const sendComposing = vi.fn(async () => undefined);
478572此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。