From 22dfb150488da8214b2884cd71990f1dca868c0c Mon Sep 17 00:00:00 2001 From: lzw112 Date: Mon, 6 Jul 2026 17:33:57 +0800 Subject: [PATCH] fix(telegram): add missing 'action' retry context for sendChatAction (#100762) * fix(telegram): add missing 'action' retry context for sendChatAction * fix(telegram): cover all sendChatAction retry paths --------- Co-authored-by: Vincent Koc --- .../telegram/src/network-errors.test.ts | 7 +++--- extensions/telegram/src/network-errors.ts | 1 + extensions/telegram/src/send.test.ts | 22 +++++++++++++++++++ extensions/telegram/src/send.ts | 2 +- .../src/sendchataction-401-backoff.test.ts | 1 + .../src/sendchataction-401-backoff.ts | 2 +- 6 files changed, 30 insertions(+), 5 deletions(-) diff --git a/extensions/telegram/src/network-errors.test.ts b/extensions/telegram/src/network-errors.test.ts index d348788a4076..8381b081952d 100644 --- a/extensions/telegram/src/network-errors.test.ts +++ b/extensions/telegram/src/network-errors.test.ts @@ -140,14 +140,15 @@ describe("isRecoverableTelegramNetworkError", () => { expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true); }); - it("treats delete/react/edit (idempotent) contexts like polling, not send", () => { + it("treats delete/react/edit/action (idempotent) contexts like polling, not send", () => { const undiciSnippetErr = new Error("Undici: socket failure"); - // delete, react, and edit are idempotent Telegram operations; a transient - // snippet-only error must be retried (allowMessageMatch defaults true), + // delete, react, edit, and action are idempotent or non-message operations; + // a transient snippet-only error must be retried (allowMessageMatch defaults true), // matching polling/webhook. send stays strict as the regression guard. expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "delete" })).toBe(true); expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "react" })).toBe(true); expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "edit" })).toBe(true); + expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "action" })).toBe(true); expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false); }); diff --git a/extensions/telegram/src/network-errors.ts b/extensions/telegram/src/network-errors.ts index 47b32a86cfe2..0cfb1e3fe143 100644 --- a/extensions/telegram/src/network-errors.ts +++ b/extensions/telegram/src/network-errors.ts @@ -148,6 +148,7 @@ export type TelegramNetworkErrorContext = | "delete" | "react" | "edit" + | "action" | "unknown"; export type TelegramNetworkErrorOrigin = { method?: string | null; diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index c7c34c151a4b..7bb87c46edfb 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -638,6 +638,28 @@ describe("sendMessageTelegram", () => { }); }); + it("retries snippet-only network errors when sending typing", async () => { + loadConfig.mockReturnValue({ + channels: { + telegram: { + botToken: "tok", + }, + }, + }); + botApi.sendChatAction + .mockRejectedValueOnce(new Error("socket hang up")) + .mockResolvedValue(true); + + await sendTypingTelegram("telegram:group:-1001234567890", { + cfg: TELEGRAM_TEST_CFG, + token: "tok", + accountId: "default", + retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, + }); + + expect(botApi.sendChatAction).toHaveBeenCalledTimes(2); + }); + it("pins and unpins Telegram messages", async () => { loadConfig.mockReturnValue({ channels: { diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index b776772d11db..26cbfc38cb8c 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -1438,7 +1438,7 @@ export async function sendTypingTelegram( account, retry: opts.retry, verbose: opts.verbose, - shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }), + shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "action" }), }); const threadParams = buildTypingThreadParams(target.messageThreadId ?? opts.messageThreadId); await requestWithDiag( diff --git a/extensions/telegram/src/sendchataction-401-backoff.test.ts b/extensions/telegram/src/sendchataction-401-backoff.test.ts index f6687950602a..6dd28eea2ed0 100644 --- a/extensions/telegram/src/sendchataction-401-backoff.test.ts +++ b/extensions/telegram/src/sendchataction-401-backoff.test.ts @@ -178,6 +178,7 @@ describe("createTelegramSendChatActionHandler", () => { it.each([ ["recoverable network", () => makeNetworkError(), 1000], + ["snippet-only network", () => new Error("socket hang up"), 1000], ["Telegram 429", () => makeTelegramError("Too Many Requests", 429, { retry_after: 2 }), 2000], ["Telegram 5xx", () => makeTelegramError("Bad Gateway", 502), 1000], ])("cools down transient %s errors", async (_name, makeError, expectedCooldownMs) => { diff --git a/extensions/telegram/src/sendchataction-401-backoff.ts b/extensions/telegram/src/sendchataction-401-backoff.ts index 2a607e183e40..29067d9580e2 100644 --- a/extensions/telegram/src/sendchataction-401-backoff.ts +++ b/extensions/telegram/src/sendchataction-401-backoff.ts @@ -100,7 +100,7 @@ function isTransientSendChatActionError(error: unknown): boolean { return ( isTelegramRateLimitError(error) || isTelegramServerError(error) || - isRecoverableTelegramNetworkError(error, { context: "send" }) + isRecoverableTelegramNetworkError(error, { context: "action" }) ); }