From c9d9067931fd1299538f497ada768b083f88ae77 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 30 Apr 2026 17:49:13 +0530 Subject: [PATCH] test(telegram): cover message-only previews --- .../telegram/src/bot-message-dispatch.test.ts | 48 +--- .../bot.create-telegram-bot.test-harness.ts | 5 - .../telegram/src/draft-stream.test-helpers.ts | 6 - extensions/telegram/src/draft-stream.test.ts | 226 ++---------------- extensions/telegram/src/lane-delivery.test.ts | 165 ------------- 5 files changed, 27 insertions(+), 423 deletions(-) diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 17bb79a291c..7c92ac2e0d4 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -747,31 +747,6 @@ describe("dispatchTelegramMessage draft streaming", () => { ); }); - it("does not materialize native draft tool progress before final-only text", async () => { - const draftStream = createTestDraftStream({ previewMode: "draft" }); - draftStream.materialize.mockResolvedValue(321); - createTelegramDraftStream.mockReturnValue(draftStream); - dispatchReplyWithBufferedBlockDispatcher.mockImplementation( - async ({ dispatcherOptions, replyOptions }) => { - await replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); - await dispatcherOptions.deliver({ text: "Done" }, { kind: "final" }); - return { queuedFinal: true }; - }, - ); - - await dispatchWithContext({ context: createContext(), streamMode: "partial" }); - - expect(draftStream.update).toHaveBeenCalledWith("Working…\n• `tool: exec`"); - expect(draftStream.update).not.toHaveBeenCalledWith("Done"); - expect(draftStream.materialize).not.toHaveBeenCalled(); - expect(deliverReplies).toHaveBeenCalledWith( - expect.objectContaining({ - replies: [expect.objectContaining({ text: "Done" })], - }), - ); - expect(draftStream.clear).toHaveBeenCalledTimes(1); - }); - it("suppresses Telegram tool progress when explicitly disabled", async () => { const draftStream = createDraftStream(); createTelegramDraftStream.mockReturnValue(draftStream); @@ -2463,13 +2438,11 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ thread: { id: 777, scope: "dm" }, - previewTransport: "message", }), ); expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual( expect.objectContaining({ thread: { id: 777, scope: "dm" }, - previewTransport: "message", }), ); }); @@ -2494,7 +2467,6 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ thread: { id: 777, scope: "dm" }, - previewTransport: "message", }), ); expect(answerDraftStream.materialize).not.toHaveBeenCalled(); @@ -2638,14 +2610,13 @@ describe("dispatchTelegramMessage draft streaming", () => { ); }); - it("keeps DM draft reasoning block updates in preview flow without sending duplicates", async () => { + it("keeps DM reasoning block updates in preview flow without sending duplicates", async () => { const answerDraftStream = createDraftStream(999); let previewRevision = 0; const reasoningDraftStream = { update: vi.fn(), flush: vi.fn().mockResolvedValue(true), - messageId: vi.fn().mockReturnValue(undefined), - previewMode: vi.fn().mockReturnValue("draft"), + messageId: vi.fn().mockReturnValue(111), previewRevision: vi.fn().mockImplementation(() => previewRevision), clear: vi.fn().mockResolvedValue(undefined), stop: vi.fn().mockResolvedValue(undefined), @@ -2680,10 +2651,16 @@ describe("dispatchTelegramMessage draft streaming", () => { await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" }); expect(editMessageTelegram).toHaveBeenCalledWith(123, 999, "3", expect.any(Object)); - expect(reasoningDraftStream.update).toHaveBeenCalledWith( + expect(editMessageTelegram).toHaveBeenCalledWith( + 123, + 111, "Reasoning:\nI am counting letters. The total is 3.", + expect.any(Object), ); - expect(reasoningDraftStream.flush).toHaveBeenCalled(); + expect(reasoningDraftStream.update).toHaveBeenCalledWith( + "Reasoning:\nI am counting letters...", + ); + expect(reasoningDraftStream.flush).not.toHaveBeenCalled(); expect(deliverReplies).not.toHaveBeenCalledWith( expect.objectContaining({ replies: [expect.objectContaining({ text: expect.stringContaining("Reasoning:\nI am") })], @@ -2691,14 +2668,13 @@ describe("dispatchTelegramMessage draft streaming", () => { ); }); - it("falls back to normal send when DM draft reasoning flush emits no preview update", async () => { + it("falls back to normal send when DM reasoning preview has no message id", async () => { const answerDraftStream = createDraftStream(999); const previewRevision = 0; const reasoningDraftStream = { update: vi.fn(), flush: vi.fn().mockResolvedValue(false), messageId: vi.fn().mockReturnValue(undefined), - previewMode: vi.fn().mockReturnValue("draft"), previewRevision: vi.fn().mockReturnValue(previewRevision), clear: vi.fn().mockResolvedValue(undefined), stop: vi.fn().mockResolvedValue(undefined), @@ -2722,7 +2698,7 @@ describe("dispatchTelegramMessage draft streaming", () => { await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" }); - expect(reasoningDraftStream.flush).toHaveBeenCalled(); + expect(reasoningDraftStream.flush).not.toHaveBeenCalled(); expect(deliverReplies).toHaveBeenCalledWith( expect.objectContaining({ replies: [expect.objectContaining({ text: "Reasoning:\n_step one expanded_" })], diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index 05c3b3ac024..529eb19fc0b 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -271,7 +271,6 @@ const grammySpies = vi.hoisted(() => ({ sendChatActionSpy: vi.fn(), editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock, editMessageReplyMarkupSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock, - sendMessageDraftSpy: vi.fn(async () => true) as AnyAsyncMock, setMessageReactionSpy: vi.fn(async () => undefined) as AnyAsyncMock, setMyCommandsSpy: vi.fn(async () => undefined) as AnyAsyncMock, getMeSpy: vi.fn(async () => ({ @@ -297,7 +296,6 @@ export const answerCallbackQuerySpy: AnyAsyncMock = grammySpies.answerCallbackQu export const sendChatActionSpy: AnyMock = grammySpies.sendChatActionSpy; export const editMessageTextSpy: AnyAsyncMock = grammySpies.editMessageTextSpy; export const editMessageReplyMarkupSpy: AnyAsyncMock = grammySpies.editMessageReplyMarkupSpy; -export const sendMessageDraftSpy: AnyAsyncMock = grammySpies.sendMessageDraftSpy; export const setMessageReactionSpy: AnyAsyncMock = grammySpies.setMessageReactionSpy; export const setMyCommandsSpy: AnyAsyncMock = grammySpies.setMyCommandsSpy; export const getMeSpy: AnyAsyncMock = grammySpies.getMeSpy; @@ -327,7 +325,6 @@ export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = { sendChatAction: grammySpies.sendChatActionSpy, editMessageText: grammySpies.editMessageTextSpy, editMessageReplyMarkup: grammySpies.editMessageReplyMarkupSpy, - sendMessageDraft: grammySpies.sendMessageDraftSpy, setMessageReaction: grammySpies.setMessageReactionSpy, setMyCommands: grammySpies.setMyCommandsSpy, getMe: grammySpies.getMeSpy, @@ -521,8 +518,6 @@ beforeEach(() => { editMessageTextSpy.mockResolvedValue({ message_id: 88 }); editMessageReplyMarkupSpy.mockReset(); editMessageReplyMarkupSpy.mockResolvedValue({ message_id: 88 }); - sendMessageDraftSpy.mockReset(); - sendMessageDraftSpy.mockResolvedValue(true); enqueueSystemEventSpy.mockReset(); wasSentByBot.mockReset(); wasSentByBot.mockReturnValue(false); diff --git a/extensions/telegram/src/draft-stream.test-helpers.ts b/extensions/telegram/src/draft-stream.test-helpers.ts index 9ef026fa2ee..1423381550b 100644 --- a/extensions/telegram/src/draft-stream.test-helpers.ts +++ b/extensions/telegram/src/draft-stream.test-helpers.ts @@ -1,13 +1,10 @@ import { vi } from "vitest"; -type DraftPreviewMode = "message" | "draft"; - export type TestDraftStream = { update: ReturnType void>>; flush: ReturnType Promise>>; messageId: ReturnType number | undefined>>; visibleSinceMs: ReturnType number | undefined>>; - previewMode: ReturnType DraftPreviewMode>>; previewRevision: ReturnType number>>; lastDeliveredText: ReturnType string>>; clear: ReturnType Promise>>; @@ -21,7 +18,6 @@ export type TestDraftStream = { export function createTestDraftStream(params?: { messageId?: number; - previewMode?: DraftPreviewMode; onUpdate?: (text: string) => void; onStop?: () => void | Promise; onDiscard?: () => void | Promise; @@ -41,7 +37,6 @@ export function createTestDraftStream(params?: { flush: vi.fn().mockResolvedValue(undefined), messageId: vi.fn().mockImplementation(() => messageId), visibleSinceMs: vi.fn().mockImplementation(() => visibleSinceMs), - previewMode: vi.fn().mockReturnValue(params?.previewMode ?? "message"), previewRevision: vi.fn().mockImplementation(() => previewRevision), lastDeliveredText: vi.fn().mockImplementation(() => lastDeliveredText), clear: vi.fn().mockResolvedValue(undefined), @@ -84,7 +79,6 @@ export function createSequencedTestDraftStream(startMessageId = 1001): TestDraft flush: vi.fn().mockResolvedValue(undefined), messageId: vi.fn().mockImplementation(() => activeMessageId), visibleSinceMs: vi.fn().mockImplementation(() => visibleSinceMs), - previewMode: vi.fn().mockReturnValue("message"), previewRevision: vi.fn().mockImplementation(() => previewRevision), lastDeliveredText: vi.fn().mockImplementation(() => lastDeliveredText), clear: vi.fn().mockResolvedValue(undefined), diff --git a/extensions/telegram/src/draft-stream.test.ts b/extensions/telegram/src/draft-stream.test.ts index c7eca6ce3ef..8940be7dfa4 100644 --- a/extensions/telegram/src/draft-stream.test.ts +++ b/extensions/telegram/src/draft-stream.test.ts @@ -1,14 +1,12 @@ import type { Bot } from "grammy"; -import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { __testing, createTelegramDraftStream } from "./draft-stream.js"; +import { createTelegramDraftStream } from "./draft-stream.js"; type TelegramDraftStreamParams = Parameters[0]; function createMockDraftApi(sendMessageImpl?: () => Promise<{ message_id: number }>) { return { sendMessage: vi.fn(sendMessageImpl ?? (async () => ({ message_id: 17 }))), - sendMessageDraft: vi.fn().mockResolvedValue(true), editMessageText: vi.fn().mockResolvedValue(true), deleteMessage: vi.fn().mockResolvedValue(true), }; @@ -53,22 +51,6 @@ function expectDmMessagePreviewViaSendMessage( expect(api.editMessageText).not.toHaveBeenCalled(); } -async function createDmDraftTransportStream(params: { - api?: ReturnType; - previewTransport?: "draft" | "message"; - warn?: (message: string) => void; -}) { - const api = params.api ?? createMockDraftApi(); - const stream = createDraftStream(api, { - thread: { id: 42, scope: "dm" }, - previewTransport: params.previewTransport ?? "draft", - ...(params.warn ? { warn: params.warn } : {}), - }); - stream.update("Hello"); - await stream.flush(); - return { api, stream }; -} - function createForceNewMessageHarness(params: { throttleMs?: number } = {}) { const api = createMockDraftApi(); api.sendMessage @@ -82,10 +64,6 @@ function createForceNewMessageHarness(params: { throttleMs?: number } = {}) { } describe("createTelegramDraftStream", () => { - afterEach(() => { - __testing.resetTelegramDraftStreamForTests(); - }); - it("sends stream preview message with message_thread_id when provided", async () => { const api = createMockDraftApi(); const stream = createForumDraftStream(api); @@ -137,31 +115,20 @@ describe("createTelegramDraftStream", () => { await vi.waitFor(() => expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", undefined)); }); - it("uses sendMessageDraft for dm threads and does not create a preview message", async () => { + it("uses sendMessage/editMessageText for dm thread previews", async () => { const api = createMockDraftApi(); const stream = createThreadedDraftStream(api, { id: 42, scope: "dm" }); stream.update("Hello"); await vi.waitFor(() => - expect(api.sendMessageDraft).toHaveBeenCalledWith(123, expect.any(Number), "Hello", { - message_thread_id: 42, - }), + expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 }), ); - expect(api.sendMessage).not.toHaveBeenCalled(); expect(api.editMessageText).not.toHaveBeenCalled(); - await stream.clear(); - expect(api.sendMessageDraft).toHaveBeenLastCalledWith(123, expect.any(Number), "", { - message_thread_id: 42, - }); - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); + stream.update("Hello again"); + await stream.flush(); - it("supports forcing message transport in dm threads", async () => { - const { api } = await createDmDraftTransportStream({ previewTransport: "message" }); - - expectDmMessagePreviewViaSendMessage(api); - expect(api.sendMessageDraft).not.toHaveBeenCalled(); + expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "Hello again"); }); it("tracks when a message preview first became visible", async () => { @@ -169,7 +136,7 @@ describe("createTelegramDraftStream", () => { try { vi.setSystemTime(new Date("2026-04-26T01:00:00.000Z")); const api = createMockDraftApi(); - const stream = createDraftStream(api, { previewTransport: "message" }); + const stream = createDraftStream(api); stream.update("Hello"); await stream.flush(); @@ -186,41 +153,6 @@ describe("createTelegramDraftStream", () => { } }); - it("falls back to message transport when sendMessageDraft is unavailable", async () => { - const api = createMockDraftApi(); - delete (api as { sendMessageDraft?: unknown }).sendMessageDraft; - const warn = vi.fn(); - await createDmDraftTransportStream({ api, warn }); - - expectDmMessagePreviewViaSendMessage(api); - expect(warn).toHaveBeenCalledWith( - "telegram stream preview: sendMessageDraft unavailable; falling back to sendMessage/editMessageText", - ); - }); - - it("falls back to message transport when sendMessageDraft is rejected at runtime", async () => { - const api = createMockDraftApi(); - api.sendMessageDraft.mockRejectedValueOnce( - new Error( - "Call to 'sendMessageDraft' failed! (400: Bad Request: method sendMessageDraft can be used only in private chats)", - ), - ); - const warn = vi.fn(); - const { stream } = await createDmDraftTransportStream({ api, warn }); - - expect(api.sendMessageDraft).toHaveBeenCalledTimes(1); - expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 }); - expect(stream.previewMode?.()).toBe("message"); - expect(warn).toHaveBeenCalledWith( - "telegram stream preview: sendMessageDraft rejected by API; falling back to sendMessage/editMessageText", - ); - - stream.update("Hello again"); - await stream.flush(); - - expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "Hello again"); - }); - it("retries DM message preview send without thread when thread is not found", async () => { const api = createMockDraftApi(); api.sendMessage @@ -229,7 +161,6 @@ describe("createTelegramDraftStream", () => { const warn = vi.fn(); const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" }, - previewTransport: "message", warn, }); @@ -247,7 +178,6 @@ describe("createTelegramDraftStream", () => { const api = createMockDraftApi(); const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" }, - previewTransport: "message", replyToMessageId: 411, }); @@ -261,11 +191,10 @@ describe("createTelegramDraftStream", () => { }); }); - it("materializes draft previews using rendered HTML text", async () => { + it("materializes message previews using rendered HTML text", async () => { const api = createMockDraftApi(); const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" }, - previewTransport: "draft", renderText: (text) => ({ text: text.replace("**bold**", "bold"), parseMode: "HTML", @@ -274,68 +203,20 @@ describe("createTelegramDraftStream", () => { stream.update("**bold**"); await stream.flush(); - await stream.materialize?.(); + const materializedId = await stream.materialize?.(); + expect(materializedId).toBe(17); expect(api.sendMessage).toHaveBeenCalledWith(123, "bold", { message_thread_id: 42, parse_mode: "HTML", }); - }); - - it("clears draft after materializing to avoid duplicate display in DM", async () => { - const api = createMockDraftApi(); - const stream = createDraftStream(api, { - thread: { id: 42, scope: "dm" }, - previewTransport: "draft", - }); - - stream.update("Hello"); - await stream.flush(); - const materializedId = await stream.materialize?.(); - - expect(materializedId).toBe(17); - expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 }); - // Draft should be cleared with empty string after real message is sent. - const draftCalls = api.sendMessageDraft.mock.calls; - const clearCall = draftCalls.find((call) => call[2] === ""); - expect(clearCall).toBeDefined(); - expect(clearCall?.[0]).toBe(123); - expect(clearCall?.[3]).toEqual({ message_thread_id: 42 }); - }); - - it("retries materialize send without thread when dm thread lookup fails", async () => { - const api = createMockDraftApi(); - api.sendMessage - .mockRejectedValueOnce(new Error("400: Bad Request: message thread not found")) - .mockResolvedValueOnce({ message_id: 55 }); - const warn = vi.fn(); - const stream = createDraftStream(api, { - thread: { id: 42, scope: "dm" }, - previewTransport: "draft", - warn, - }); - - stream.update("Hello"); - await stream.flush(); - const materializedId = await stream.materialize?.(); - - expect(materializedId).toBe(55); - expect(api.sendMessage).toHaveBeenNthCalledWith(1, 123, "Hello", { message_thread_id: 42 }); - expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "Hello", undefined); - const draftCalls = api.sendMessageDraft.mock.calls; - const clearCall = draftCalls.find((call) => call[2] === ""); - expect(clearCall).toBeDefined(); - expect(clearCall?.[3]).toBeUndefined(); - expect(warn).toHaveBeenCalledWith( - "telegram stream preview materialize send failed with message_thread_id, retrying without thread", - ); + expect(api.sendMessage).toHaveBeenCalledTimes(1); }); it("returns existing preview id when materializing message transport", async () => { const api = createMockDraftApi(); const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" }, - previewTransport: "message", }); stream.update("Hello"); @@ -346,7 +227,7 @@ describe("createTelegramDraftStream", () => { expect(api.sendMessage).toHaveBeenCalledTimes(1); }); - it("does not edit or delete messages after DM draft stream finalization", async () => { + it("deletes message preview on clear after finalization", async () => { const api = createMockDraftApi(); const stream = createThreadedDraftStream(api, { id: 42, scope: "dm" }); @@ -356,86 +237,9 @@ describe("createTelegramDraftStream", () => { await stream.stop(); await stream.clear(); - expect(api.sendMessageDraft).toHaveBeenCalled(); - expect(api.sendMessage).not.toHaveBeenCalled(); - expect(api.editMessageText).not.toHaveBeenCalled(); - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); - - it("rotates draft_id when forceNewMessage races an in-flight DM draft send", async () => { - let resolveFirstDraft: ((value: boolean) => void) | undefined; - const firstDraftSend = new Promise((resolve) => { - resolveFirstDraft = resolve; - }); - const api = { - sendMessageDraft: vi.fn().mockReturnValueOnce(firstDraftSend).mockResolvedValueOnce(true), - sendMessage: vi.fn().mockResolvedValue({ message_id: 17 }), - editMessageText: vi.fn().mockResolvedValue(true), - deleteMessage: vi.fn().mockResolvedValue(true), - }; - const stream = createThreadedDraftStream( - api as unknown as ReturnType, - { id: 42, scope: "dm" }, - ); - - stream.update("Message A"); - await vi.waitFor(() => expect(api.sendMessageDraft).toHaveBeenCalledTimes(1)); - - stream.forceNewMessage(); - stream.update("Message B"); - - resolveFirstDraft?.(true); - await stream.flush(); - - expect(api.sendMessageDraft).toHaveBeenCalledTimes(2); - const firstDraftId = api.sendMessageDraft.mock.calls[0]?.[1]; - const secondDraftId = api.sendMessageDraft.mock.calls[1]?.[1]; - expect(typeof firstDraftId).toBe("number"); - expect(typeof secondDraftId).toBe("number"); - expect(firstDraftId).not.toBe(secondDraftId); - expect(api.sendMessageDraft.mock.calls[1]?.[2]).toBe("Message B"); - expect(api.sendMessage).not.toHaveBeenCalled(); - expect(api.editMessageText).not.toHaveBeenCalled(); - }); - - it("shares draft-id allocation across distinct module instances", async () => { - const draftA = await importFreshModule( - import.meta.url, - "./draft-stream.js?scope=shared-a", - ); - const draftB = await importFreshModule( - import.meta.url, - "./draft-stream.js?scope=shared-b", - ); - const apiA = createMockDraftApi(); - const apiB = createMockDraftApi(); - - draftA.__testing.resetTelegramDraftStreamForTests(); - - try { - const streamA = draftA.createTelegramDraftStream({ - api: apiA as unknown as Bot["api"], - chatId: 123, - thread: { id: 42, scope: "dm" }, - previewTransport: "draft", - }); - const streamB = draftB.createTelegramDraftStream({ - api: apiB as unknown as Bot["api"], - chatId: 123, - thread: { id: 42, scope: "dm" }, - previewTransport: "draft", - }); - - streamA.update("Message A"); - await streamA.flush(); - streamB.update("Message B"); - await streamB.flush(); - - expect(apiA.sendMessageDraft.mock.calls[0]?.[1]).toBe(1); - expect(apiB.sendMessageDraft.mock.calls[0]?.[1]).toBe(2); - } finally { - draftA.__testing.resetTelegramDraftStreamForTests(); - } + expect(api.sendMessage).toHaveBeenCalledWith(123, "Hello", { message_thread_id: 42 }); + expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "Hello again"); + expect(api.deleteMessage).toHaveBeenCalledWith(123, 17); }); it("creates new message after forceNewMessage is called", async () => { diff --git a/extensions/telegram/src/lane-delivery.test.ts b/extensions/telegram/src/lane-delivery.test.ts index adbabaa20e4..ac274a6d50c 100644 --- a/extensions/telegram/src/lane-delivery.test.ts +++ b/extensions/telegram/src/lane-delivery.test.ts @@ -493,171 +493,6 @@ describe("createLaneTextDeliverer", () => { expect(harness.markDelivered).toHaveBeenCalledTimes(1); }); - it("materializes DM draft streaming final even when text is unchanged", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 321 }); - answerStream.materialize.mockResolvedValue(321); - answerStream.update.mockImplementation(() => {}); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: true, - answerLastPartialText: "Hello final", - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Hello final", - payload: { text: "Hello final" }, - infoKind: "final", - }); - - expect(expectPreviewFinalized(result)).toEqual({ content: "Hello final", messageId: 321 }); - expect(harness.flushDraftLane).toHaveBeenCalled(); - expect(answerStream.materialize).toHaveBeenCalledTimes(1); - expect(harness.sendPayload).not.toHaveBeenCalled(); - expect(harness.markDelivered).toHaveBeenCalledTimes(1); - }); - - it("does not materialize a native draft for final-only text", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); - answerStream.materialize.mockResolvedValue(321); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: false, - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Final only", - payload: { text: "Final only" }, - infoKind: "final", - }); - - expect(result.kind).toBe("sent"); - expect(answerStream.update).not.toHaveBeenCalled(); - expect(answerStream.materialize).not.toHaveBeenCalled(); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Final only" }), - ); - }); - - it("does not materialize native draft tool-progress preview before final-only text", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); - answerStream.materialize.mockResolvedValue(321); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: false, - answerLastPartialText: "Working...\n- tool: exec", - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Final only", - payload: { text: "Final only" }, - infoKind: "final", - }); - - expect(result.kind).toBe("sent"); - expect(answerStream.update).not.toHaveBeenCalledWith("Final only"); - expect(answerStream.materialize).not.toHaveBeenCalled(); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Final only" }), - ); - }); - - it("materializes DM draft streaming final when revision changes", async () => { - let previewRevision = 3; - const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 654 }); - answerStream.materialize.mockResolvedValue(654); - answerStream.previewRevision.mockImplementation(() => previewRevision); - answerStream.update.mockImplementation(() => {}); - answerStream.flush.mockImplementation(async () => { - previewRevision += 1; - }); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: true, - answerLastPartialText: "Final answer", - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Final answer", - payload: { text: "Final answer" }, - infoKind: "final", - }); - - expect(expectPreviewFinalized(result)).toEqual({ content: "Final answer", messageId: 654 }); - expect(answerStream.materialize).toHaveBeenCalledTimes(1); - expect(harness.sendPayload).not.toHaveBeenCalled(); - expect(harness.markDelivered).toHaveBeenCalledTimes(1); - }); - - it("falls back to normal send when draft materialize returns no message id", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); - answerStream.materialize.mockResolvedValue(undefined); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: true, - answerLastPartialText: "Hello final", - }); - - const result = await deliverFinalAnswer(harness, HELLO_FINAL); - - expect(result.kind).toBe("sent"); - expect(answerStream.materialize).toHaveBeenCalledTimes(1); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: HELLO_FINAL }), - ); - expect(harness.log).toHaveBeenCalledWith( - expect.stringContaining("draft preview materialize produced no message id"), - ); - }); - - it("does not use DM draft final shortcut for media payloads", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: true, - answerLastPartialText: "Image incoming", - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Image incoming", - payload: { text: "Image incoming", mediaUrl: "file:///tmp/example.png" }, - infoKind: "final", - }); - - expect(result.kind).toBe("sent"); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Image incoming", mediaUrl: "file:///tmp/example.png" }), - ); - expect(harness.markDelivered).not.toHaveBeenCalled(); - }); - - it("does not use DM draft final shortcut when inline buttons are present", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); - const harness = createHarness({ - answerStream: answerStream as DraftLaneState["stream"], - answerHasStreamedMessage: true, - answerLastPartialText: "Choose one", - }); - - const result = await harness.deliverLaneText({ - laneName: "answer", - text: "Choose one", - payload: { text: "Choose one" }, - previewButtons: [[{ text: "OK", callback_data: "ok" }]], - infoKind: "final", - }); - - expect(result.kind).toBe("sent"); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Choose one" }), - ); - expect(harness.markDelivered).not.toHaveBeenCalled(); - }); - // ── Duplicate message regression tests ────────────────────────────────── // During final delivery, only ambiguous post-connect failures keep the // preview. Definite non-delivery falls back to a real send.