From 7a88117f429c5dee03ebf3710e29c2c1a6230fb1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 28 Apr 2026 18:24:47 -0700 Subject: [PATCH] fix(qa): retry transient Telegram polling failures --- .../telegram/telegram-live.runtime.test.ts | 63 +++++++++++++++++++ .../telegram/telegram-live.runtime.ts | 17 ++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts index bcf62f1d904..f4a9389aa44 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts @@ -474,6 +474,69 @@ describe("telegram live qa runtime", () => { ).toBe(false); }); + it("retries transient Telegram polling fetch failures while waiting for scenario replies", async () => { + const fetchMock = vi + .fn() + .mockRejectedValueOnce(new TypeError("fetch failed")) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + ok: true, + result: [ + { + update_id: 10, + message: { + message_id: 99, + chat: { id: -100123 }, + from: { id: 88, is_bot: true, username: "sut_bot" }, + text: "Identity\nChannel: telegram", + date: 1_700_000_000, + reply_to_message: { message_id: 55 }, + }, + }, + ], + }), + { + status: 200, + headers: { + "content-type": "application/json", + }, + }, + ), + ); + vi.stubGlobal("fetch", fetchMock); + const observedMessages: Parameters< + typeof __testing.waitForObservedMessage + >[0]["observedMessages"] = []; + + const result = await __testing.waitForObservedMessage({ + token: "token", + initialOffset: 7, + timeoutMs: 5_000, + observedMessages, + observationScenarioId: "telegram-whoami-command", + observationScenarioTitle: "Telegram whoami reply", + predicate: (message) => + __testing.matchesTelegramScenarioReply({ + groupId: "-100123", + message, + sentMessageId: 55, + sutBotId: 88, + }), + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(result.message.messageId).toBe(99); + expect(result.nextOffset).toBe(11); + expect(observedMessages).toEqual([ + expect.objectContaining({ + matchedScenario: true, + messageId: 99, + scenarioId: "telegram-whoami-command", + }), + ]); + }); + it("redacts observed message content by default in artifacts", () => { expect( __testing.buildObservedMessagesArtifact({ diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts index d0c80a44983..d444f486911 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts @@ -595,6 +595,10 @@ async function sendGroupMessage(token: string, groupId: string, text: string) { }); } +async function waitForTelegramPollRetryDelay(remainingMs: number) { + await new Promise((resolve) => setTimeout(resolve, Math.min(250, Math.max(100, remainingMs)))); +} + async function waitForObservedMessage(params: { token: string; initialOffset: number; @@ -606,6 +610,7 @@ async function waitForObservedMessage(params: { }) { const startedAt = Date.now(); let offset = params.initialOffset; + let lastPollingError: unknown; while (Date.now() - startedAt < params.timeoutMs) { const remainingMs = Math.max( 1_000, @@ -624,10 +629,13 @@ async function waitForObservedMessage(params: { }, timeoutSeconds * 1000 + 5_000, ); + lastPollingError = undefined; } catch (error) { if (!isRecoverableTelegramQaPollError(error)) { throw error; } + lastPollingError = error; + await waitForTelegramPollRetryDelay(params.timeoutMs - (Date.now() - startedAt)); continue; } const batchObservedAtMs = Date.now(); @@ -653,7 +661,13 @@ async function waitForObservedMessage(params: { } } } - throw new Error(`timed out after ${params.timeoutMs}ms waiting for Telegram message`); + const timeoutMessage = `timed out after ${params.timeoutMs}ms waiting for Telegram message`; + if (lastPollingError) { + throw new Error( + `${timeoutMessage}; last polling error: ${formatErrorMessage(lastPollingError)}`, + ); + } + throw new Error(timeoutMessage); } async function waitForTelegramChannelRunning( @@ -1472,4 +1486,5 @@ export const __testing = { shouldLogTelegramQaLiveProgress, formatTelegramQaProgressDetails, renderTelegramQaMarkdown, + waitForObservedMessage, };