From 474c4e671e9f3dfa7785d068b5fdbfb2dad5772e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 2 Aug 2026 15:32:46 -0700 Subject: [PATCH] fix(telegram): preserve tables across legacy message delivery (#118257) Co-authored-by: Peter Steinberger --- extensions/telegram/src/bot/delivery.test.ts | 19 ++++++++++++++ extensions/telegram/src/format.test.ts | 20 ++++++++++++--- extensions/telegram/src/format.ts | 26 +++++++++----------- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/extensions/telegram/src/bot/delivery.test.ts b/extensions/telegram/src/bot/delivery.test.ts index a6de461a8e30..81ca014f3f6e 100644 --- a/extensions/telegram/src/bot/delivery.test.ts +++ b/extensions/telegram/src/bot/delivery.test.ts @@ -323,6 +323,25 @@ describe("deliverReplies", () => { expect(firstMockCallArg(sendMessage, 1)).toBe("hello"); }); + it("keeps native-command tables visible in non-rich block-mode replies", async () => { + const { runtime, sendMessage, bot } = createSendMessageHarness(); + const table = "| A | B |\n| --- | --- |\n| 1 | 2 |"; + + await deliverWith({ + replies: [{ text: `Before\n\n${table}\n\nAfter` }], + runtime, + bot, + tableMode: "block", + }); + + expect(sendMessage).toHaveBeenCalledTimes(1); + const sent = firstSendText(sendMessage); + expect(sent).toContain("Before"); + expect(sent).toContain(`
${table}\n
`); + expect(sent).toContain("After"); + expectRecordFields(firstMockCallArg(sendMessage, 2), { parse_mode: "HTML" }); + }); + it("delivers prepared HTML without reparsing visible syntax as Markdown", async () => { const runtime = createRuntime(); const sendMessage = vi.fn().mockResolvedValue({ message_id: 1, chat: { id: "123" } }); diff --git a/extensions/telegram/src/format.test.ts b/extensions/telegram/src/format.test.ts index 77fc0cb35d7e..e62abc150b4c 100644 --- a/extensions/telegram/src/format.test.ts +++ b/extensions/telegram/src/format.test.ts @@ -139,12 +139,24 @@ describe("markdownToTelegramHtml", () => { ); }); - it("renders block-mode tables as code in legacy Telegram HTML", () => { + it.each([ + { name: "a table-only reply", before: "", after: "" }, + { name: "a table between surrounding prose", before: "Before\n\n", after: "\n\nAfter" }, + ])("keeps $name visible in one-shot and chunked legacy Telegram HTML", ({ before, after }) => { const table = "| A | B |\n| --- | --- |\n| 1 | 2 |"; + const markdown = `${before}${table}${after}`; + const html = markdownToTelegramHtml(markdown, { tableMode: "block" }); + const chunks = markdownToTelegramChunks(markdown, 4096, { tableMode: "block" }); - expect(markdownToTelegramHtml(table, { tableMode: "block" })).toBe( - "
| A | B |\n| --- | --- |\n| 1 | 2 |\n
", - ); + expect(html).toContain(`
${table}\n
`); + expect(chunks.map((chunk) => chunk.html)).toEqual([html]); + expect(chunks[0]?.text).toContain("| 1 | 2 |"); + if (before) { + expect(html).toContain("Before"); + } + if (after) { + expect(html).toContain("After"); + } }); it("normalizes raw code language HTML without leaking tags", () => { diff --git a/extensions/telegram/src/format.ts b/extensions/telegram/src/format.ts index 2670b60f9368..7efc4899ad79 100644 --- a/extensions/telegram/src/format.ts +++ b/extensions/telegram/src/format.ts @@ -147,19 +147,22 @@ function preserveTelegramListBoundarySpacing(markdown: string): string { return out.join("\n"); } -export function markdownToTelegramHtml( - markdown: string, - options: { tableMode?: MarkdownTableMode; wrapFileRefs?: boolean } = {}, -): string { - const tableMode = options.tableMode === "block" ? "code" : options.tableMode; - const ir = markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), { +function parseTelegramLegacyMarkdown(markdown: string, tableMode?: MarkdownTableMode): MarkdownIR { + return markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), { assistantTranscriptRoleHeaders: true, linkify: true, enableSpoilers: true, headingStyle: "none", blockquotePrefix: "", - tableMode, + tableMode: tableMode === "block" ? "code" : tableMode, }); +} + +export function markdownToTelegramHtml( + markdown: string, + options: { tableMode?: MarkdownTableMode; wrapFileRefs?: boolean } = {}, +): string { + const ir = parseTelegramLegacyMarkdown(markdown, options.tableMode); const html = renderTelegramHtml(ir); const telegramHtml = renderSupportedTelegramHtml(html); // Apply file reference wrapping if requested (for chunked rendering) @@ -910,14 +913,7 @@ export function markdownToTelegramChunks( limit: number, options: { tableMode?: MarkdownTableMode } = {}, ): TelegramFormattedChunk[] { - const ir = markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), { - assistantTranscriptRoleHeaders: true, - linkify: true, - enableSpoilers: true, - headingStyle: "none", - blockquotePrefix: "", - tableMode: options.tableMode, - }); + const ir = parseTelegramLegacyMarkdown(markdown, options.tableMode); return renderTelegramChunksWithinHtmlLimit(ir, limit); }