diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts
index ce812a0ea59..ed839212dfb 100644
--- a/src/telegram/send.test.ts
+++ b/src/telegram/send.test.ts
@@ -1,5 +1,5 @@
import type { Bot } from "grammy";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import {
getTelegramSendTestMocks,
importTelegramSendModule,
@@ -40,6 +40,22 @@ async function expectChatNotFoundWithChatId(
}
}
+function mockLoadedMedia({
+ buffer = Buffer.from("media"),
+ contentType,
+ fileName,
+}: {
+ buffer?: Buffer;
+ contentType?: string;
+ fileName?: string;
+}): void {
+ loadWebMedia.mockResolvedValueOnce({
+ buffer,
+ ...(contentType ? { contentType } : {}),
+ ...(fileName ? { fileName } : {}),
+ });
+}
+
describe("sent-message-cache", () => {
afterEach(() => {
clearSentMessageCache();
@@ -189,34 +205,81 @@ describe("sendMessageTelegram", () => {
}
});
- it("falls back to plain text when Telegram rejects HTML", async () => {
- const chatId = "123";
+ it("falls back to plain text when Telegram rejects HTML and preserves send params", async () => {
const parseErr = new Error(
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9",
);
- const sendMessage = vi
- .fn()
- .mockRejectedValueOnce(parseErr)
- .mockResolvedValueOnce({
- message_id: 42,
- chat: { id: chatId },
+ const cases = [
+ {
+ name: "plain text send",
+ chatId: "123",
+ text: "_oops_",
+ htmlText: "oops",
+ messageId: 42,
+ options: { verbose: true } as const,
+ firstCall: { parse_mode: "HTML" },
+ secondCall: undefined,
+ },
+ {
+ name: "threaded reply send",
+ chatId: "-1001234567890",
+ text: "_bad markdown_",
+ htmlText: "bad markdown",
+ messageId: 60,
+ options: { messageThreadId: 271, replyToMessageId: 100 } as const,
+ firstCall: {
+ parse_mode: "HTML",
+ message_thread_id: 271,
+ reply_to_message_id: 100,
+ },
+ secondCall: {
+ message_thread_id: 271,
+ reply_to_message_id: 100,
+ },
+ },
+ ] as const;
+
+ for (const testCase of cases) {
+ const sendMessage = vi
+ .fn()
+ .mockRejectedValueOnce(parseErr)
+ .mockResolvedValueOnce({
+ message_id: testCase.messageId,
+ chat: { id: testCase.chatId },
+ });
+ const api = { sendMessage } as unknown as {
+ sendMessage: typeof sendMessage;
+ };
+
+ const res = await sendMessageTelegram(testCase.chatId, testCase.text, {
+ token: "tok",
+ api,
+ ...testCase.options,
});
- const api = { sendMessage } as unknown as {
- sendMessage: typeof sendMessage;
- };
- const res = await sendMessageTelegram(chatId, "_oops_", {
- token: "tok",
- api,
- verbose: true,
- });
-
- expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "oops", {
- parse_mode: "HTML",
- });
- expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_oops_");
- expect(res.chatId).toBe(chatId);
- expect(res.messageId).toBe("42");
+ expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
+ 1,
+ testCase.chatId,
+ testCase.htmlText,
+ testCase.firstCall,
+ );
+ if (testCase.secondCall) {
+ expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
+ 2,
+ testCase.chatId,
+ testCase.text,
+ testCase.secondCall,
+ );
+ } else {
+ expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
+ 2,
+ testCase.chatId,
+ testCase.text,
+ );
+ }
+ expect(res.chatId, testCase.name).toBe(testCase.chatId);
+ expect(res.messageId, testCase.name).toBe(String(testCase.messageId));
+ }
});
it("keeps link_preview_options disabled for both html and plain-text fallback", async () => {
@@ -306,41 +369,6 @@ describe("sendMessageTelegram", () => {
});
});
- it("preserves thread params in plain text fallback", async () => {
- const chatId = "-1001234567890";
- const parseErr = new Error(
- "400: Bad Request: can't parse entities: Can't find end of the entity",
- );
- const sendMessage = vi
- .fn()
- .mockRejectedValueOnce(parseErr)
- .mockResolvedValueOnce({
- message_id: 60,
- chat: { id: chatId },
- });
- const api = { sendMessage } as unknown as {
- sendMessage: typeof sendMessage;
- };
-
- const res = await sendMessageTelegram(chatId, "_bad markdown_", {
- token: "tok",
- api,
- messageThreadId: 271,
- replyToMessageId: 100,
- });
-
- expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "bad markdown", {
- parse_mode: "HTML",
- message_thread_id: 271,
- reply_to_message_id: 100,
- });
- expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_bad markdown_", {
- message_thread_id: 271,
- reply_to_message_id: 100,
- });
- expect(res.messageId).toBe("60");
- });
-
it("includes thread params in media messages", async () => {
const chatId = "-1001234567890";
const sendPhoto = vi.fn().mockResolvedValue({
@@ -351,7 +379,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-image"),
contentType: "image/jpeg",
fileName: "photo.jpg",
@@ -388,7 +416,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-image"),
contentType: "image/jpeg",
fileName: "photo.jpg",
@@ -423,7 +451,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-image"),
contentType: "image/jpeg",
fileName: "photo.jpg",
@@ -455,7 +483,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-image"),
contentType: "image/jpeg",
fileName: "photo.jpg",
@@ -491,7 +519,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-video"),
contentType: "video/mp4",
fileName: "video.mp4",
@@ -521,7 +549,7 @@ describe("sendMessageTelegram", () => {
sendVideo: typeof sendVideo;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-video"),
contentType: "video/mp4",
fileName: "video.mp4",
@@ -590,7 +618,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-video"),
contentType: "video/mp4",
fileName: "video.mp4",
@@ -680,7 +708,7 @@ describe("sendMessageTelegram", () => {
sendAnimation: typeof sendAnimation;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("GIF89a"),
fileName: "fun.gif",
});
@@ -779,7 +807,7 @@ describe("sendMessageTelegram", () => {
sendVoice: typeof sendVoice;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("audio"),
contentType: testCase.contentType,
fileName: testCase.fileName,
@@ -1006,7 +1034,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto;
};
- loadWebMedia.mockResolvedValueOnce({
+ mockLoadedMedia({
buffer: Buffer.from("fake-image"),
contentType: "image/jpeg",
fileName: "photo.jpg",
@@ -1075,26 +1103,18 @@ describe("reactMessageTelegram", () => {
});
describe("sendStickerTelegram", () => {
- beforeEach(() => {
- loadConfig.mockReturnValue({});
- botApi.sendSticker.mockReset();
- botCtorSpy.mockReset();
- });
-
const positiveSendCases = [
{
name: "sends a sticker by file_id",
fileId: "CAACAgIAAxkBAAI...sticker_file_id",
expectedFileId: "CAACAgIAAxkBAAI...sticker_file_id",
expectedMessageId: 100,
- assertResult: true,
},
{
name: "trims whitespace from fileId",
fileId: " fileId123 ",
expectedFileId: "fileId123",
expectedMessageId: 106,
- assertResult: false,
},
] as const;
@@ -1115,10 +1135,8 @@ describe("sendStickerTelegram", () => {
});
expect(sendSticker).toHaveBeenCalledWith(chatId, testCase.expectedFileId, undefined);
- if (testCase.assertResult) {
- expect(res.messageId).toBe(String(testCase.expectedMessageId));
- expect(res.chatId).toBe(chatId);
- }
+ expect(res.messageId).toBe(String(testCase.expectedMessageId));
+ expect(res.chatId).toBe(chatId);
});
}
@@ -1253,11 +1271,6 @@ describe("shared send behaviors", () => {
});
describe("editMessageTelegram", () => {
- beforeEach(() => {
- botApi.editMessageText.mockReset();
- botCtorSpy.mockReset();
- });
-
it("handles button payload + parse fallback behavior", async () => {
const cases: Array<{
name: string;