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 <vincentkoc@ieee.org>
This commit is contained in:
lzw112
2026-07-06 17:33:57 +08:00
committed by GitHub
parent a13a198a4b
commit 22dfb15048
6 changed files with 30 additions and 5 deletions

View File

@@ -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);
});

View File

@@ -148,6 +148,7 @@ export type TelegramNetworkErrorContext =
| "delete"
| "react"
| "edit"
| "action"
| "unknown";
export type TelegramNetworkErrorOrigin = {
method?: string | null;

View File

@@ -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: {

View File

@@ -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(

View File

@@ -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) => {

View File

@@ -100,7 +100,7 @@ function isTransientSendChatActionError(error: unknown): boolean {
return (
isTelegramRateLimitError(error) ||
isTelegramServerError(error) ||
isRecoverableTelegramNetworkError(error, { context: "send" })
isRecoverableTelegramNetworkError(error, { context: "action" })
);
}