






















@@ -5,6 +5,7 @@ import {
55createPluginStateSyncKeyedStoreForTests,
66resetPluginStateStoreForTests,
77} from "openclaw/plugin-sdk/plugin-state-test-runtime";
8+import { setReplyPayloadMetadata } from "openclaw/plugin-sdk/reply-payload-testing";
89import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
910import { resolveAutoTopicLabelConfig as resolveAutoTopicLabelConfigRuntime } from "./auto-topic-label-config.js";
1011import type { TelegramBotDeps } from "./bot-deps.js";
@@ -2612,6 +2613,73 @@ describe("dispatchTelegramMessage draft streaming", () => {
26122613expectDeliveredReply(0, { text: "Boom" });
26132614});
261426152616+it("suppresses failed tool payloads after the final reply", async () => {
2617+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
2618+await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
2619+await dispatcherOptions.deliver(
2620+{ text: "Tool failed after final", isError: true },
2621+{ kind: "tool" },
2622+);
2623+return { queuedFinal: true };
2624+});
2625+2626+await dispatchWithContext({ context: createContext(), streamMode: "off" });
2627+2628+expect(deliverReplies).toHaveBeenCalledTimes(1);
2629+expectDeliveredReply(0, { text: "Final answer" });
2630+});
2631+2632+it("preserves final error warnings after the final reply", async () => {
2633+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
2634+await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
2635+await dispatcherOptions.deliver({ text: "Write failed", isError: true }, { kind: "final" });
2636+return { queuedFinal: true };
2637+});
2638+2639+await dispatchWithContext({ context: createContext(), streamMode: "off" });
2640+2641+expect(deliverReplies).toHaveBeenCalledTimes(2);
2642+expectDeliveredReply(0, { text: "Final answer" });
2643+expectDeliveredReply(0, { text: "Write failed", isError: true }, 1);
2644+});
2645+2646+it("suppresses non-terminal final error warnings after the final reply", async () => {
2647+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
2648+await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
2649+await dispatcherOptions.deliver(
2650+setReplyPayloadMetadata(
2651+{ text: "Post-processing failed", isError: true },
2652+{ nonTerminalToolErrorWarning: true },
2653+),
2654+{ kind: "final" },
2655+);
2656+return { queuedFinal: true };
2657+});
2658+2659+await dispatchWithContext({ context: createContext(), streamMode: "off" });
2660+2661+expect(deliverReplies).toHaveBeenCalledTimes(1);
2662+expectDeliveredReply(0, { text: "Final answer" });
2663+});
2664+2665+it("preserves non-terminal final error warnings before any final reply is delivered", async () => {
2666+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
2667+await dispatcherOptions.deliver(
2668+setReplyPayloadMetadata(
2669+{ text: "Post-processing failed", isError: true },
2670+{ nonTerminalToolErrorWarning: true },
2671+),
2672+{ kind: "final" },
2673+);
2674+return { queuedFinal: true };
2675+});
2676+2677+await dispatchWithContext({ context: createContext(), streamMode: "off" });
2678+2679+expect(deliverReplies).toHaveBeenCalledTimes(1);
2680+expectDeliveredReply(0, { text: "Post-processing failed", isError: true });
2681+});
2682+26152683it("streams button-bearing text into the same message", async () => {
26162684const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
26172685const buttons = [[{ text: "OK", callback_data: "ok" }]];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。