
























@@ -652,6 +652,35 @@ describe("dispatchTelegramMessage draft streaming", () => {
652652expect(deliverReplies).not.toHaveBeenCalled();
653653});
654654655+it("suppresses text-only tool output after media-only final Telegram replies", async () => {
656+deliverInboundReplyWithMessageSendContext.mockResolvedValue({
657+status: "handled_visible",
658+delivery: {
659+messageIds: ["1002"],
660+visibleReplySent: true,
661+},
662+});
663+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
664+await dispatcherOptions.deliver({ mediaUrl: "file:///tmp/final.png" }, { kind: "final" });
665+await dispatcherOptions.deliver({ text: "late tool output" }, { kind: "tool" });
666+return { queuedFinal: true };
667+});
668+669+await dispatchWithContext({
670+context: createContext(),
671+streamMode: "off",
672+telegramDeps: telegramDepsForTest,
673+});
674+675+expect(deliverInboundReplyWithMessageSendContext).toHaveBeenCalledTimes(1);
676+const outbound = expectRecordFields(mockCallArg(deliverInboundReplyWithMessageSendContext), {
677+channel: "telegram",
678+info: { kind: "final" },
679+});
680+expectRecordFields(outbound.payload, { mediaUrl: "file:///tmp/final.png" });
681+expect(deliverReplies).not.toHaveBeenCalled();
682+});
683+655684it("skips answer draft stream for same-chat selected quotes", async () => {
656685dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
657686await dispatcherOptions.deliver({ text: "Hello", replyToId: "1001" }, { kind: "final" });
@@ -943,6 +972,24 @@ describe("dispatchTelegramMessage draft streaming", () => {
943972});
944973});
945974975+it("suppresses text-only tool payloads delivered after the final answer", async () => {
976+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
977+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
978+await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" });
979+await dispatcherOptions.deliver(
980+{ text: "failed command output", isError: true },
981+{ kind: "tool" },
982+);
983+return { queuedFinal: true };
984+});
985+986+await dispatchWithContext({ context: createContext() });
987+988+expect(answerDraftStream.update).toHaveBeenCalledTimes(1);
989+expect(answerDraftStream.update).toHaveBeenCalledWith("Final answer");
990+expect(deliverReplies).not.toHaveBeenCalled();
991+});
992+946993it("mirrors preview-finalized finals into the session transcript", async () => {
947994setupDraftStreams({ answerMessageId: 2001 });
948995const context = createContext();
@@ -1627,6 +1674,66 @@ describe("dispatchTelegramMessage draft streaming", () => {
16271674expectDeliveredReply(0, { text: "Branch is up to date" });
16281675});
162916761677+it("does not restart progress drafts for command output after final answer delivery", async () => {
1678+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1679+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1680+async ({ dispatcherOptions, replyOptions }) => {
1681+await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1682+await dispatcherOptions.deliver({ text: "Branch is up to date" }, { kind: "final" });
1683+await replyOptions?.onCommandOutput?.({
1684+phase: "end",
1685+title: "Exec",
1686+name: "exec",
1687+status: "failed",
1688+exitCode: 1,
1689+});
1690+return { queuedFinal: true };
1691+},
1692+);
1693+1694+await dispatchWithContext({
1695+context: createContext(),
1696+streamMode: "progress",
1697+telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
1698+});
1699+1700+expect(answerDraftStream.update).toHaveBeenCalledTimes(1);
1701+expect(answerDraftStream.update).toHaveBeenCalledWith("Shelling\n\n`🛠️ Exec`");
1702+expectDeliveredReply(0, { text: "Branch is up to date" });
1703+});
1704+1705+it("does not restart progress drafts for command output while final answer delivery is pending", async () => {
1706+const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1707+dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1708+async ({ dispatcherOptions, replyOptions }) => {
1709+await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1710+const finalDelivery = dispatcherOptions.deliver(
1711+{ text: "Branch is up to date" },
1712+{ kind: "final" },
1713+);
1714+await replyOptions?.onCommandOutput?.({
1715+phase: "end",
1716+title: "Exec",
1717+name: "exec",
1718+status: "failed",
1719+exitCode: 1,
1720+});
1721+await finalDelivery;
1722+return { queuedFinal: true };
1723+},
1724+);
1725+1726+await dispatchWithContext({
1727+context: createContext(),
1728+streamMode: "progress",
1729+telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
1730+});
1731+1732+expect(answerDraftStream.update).toHaveBeenCalledTimes(1);
1733+expect(answerDraftStream.update).toHaveBeenCalledWith("Shelling\n\n`🛠️ Exec`");
1734+expectDeliveredReply(0, { text: "Branch is up to date" });
1735+});
1736+16301737it("uses the transcript final when progress-mode final text is truncated", async () => {
16311738setupDraftStreams({ answerMessageId: 2001 });
16321739const fullAnswer =
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。