diff --git a/.github/workflows/qa-live-transports-convex.yml b/.github/workflows/qa-live-transports-convex.yml index b7105036e60..bbb5d7a6a12 100644 --- a/.github/workflows/qa-live-transports-convex.yml +++ b/.github/workflows/qa-live-transports-convex.yml @@ -532,6 +532,7 @@ jobs: OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000" OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" + OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000" INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }} run: | set -euo pipefail 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 eea2a39c0db..b27ea99a382 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 @@ -156,6 +156,54 @@ describe("telegram live qa runtime", () => { expect(gateway.call).toHaveBeenCalledTimes(2); }); + it("normalizes the Telegram QA transport ready timeout env", () => { + expect(testing.resolveTelegramQaReadyTimeoutMs({})).toBe(45_000); + expect( + testing.resolveTelegramQaReadyTimeoutMs({ + OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000", + }), + ).toBe(180_000); + expect( + testing.resolveTelegramQaReadyTimeoutMs({ + OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "bad", + }), + ).toBe(45_000); + for (const value of ["0x10", "1e3", "10.5"]) { + expect( + testing.resolveTelegramQaReadyTimeoutMs({ + OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: value, + }), + ).toBe(45_000); + } + }); + + it("includes the last Telegram readiness status when the account stays unavailable", async () => { + const gateway = { + call: vi.fn().mockResolvedValue({ + channelAccounts: { + telegram: [ + { + accountId: "sut", + connected: false, + lastError: "Telegram getUpdates conflict", + restartPending: true, + running: true, + }, + ], + }, + }), + }; + + await expect( + testing.waitForTelegramChannelRunning(gateway as never, "sut", { + pollMs: 1, + timeoutMs: 5, + }), + ).rejects.toThrow( + 'telegram account "sut" did not become ready; last status: {"connected":false,"lastError":"Telegram getUpdates conflict","restartPending":true,"running":true}', + ); + }); + it("normalizes the Telegram QA canary timeout env", () => { expect(testing.resolveTelegramQaCanaryTimeoutMs({})).toBe(30_000); expect( 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 92bf2cc2f38..8c358cefa02 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 @@ -126,6 +126,7 @@ type TelegramObservedMessage = { }; const DEFAULT_TELEGRAM_QA_CANARY_TIMEOUT_MS = 30_000; +const TELEGRAM_QA_DEFAULT_READY_TIMEOUT_MS = 45_000; type TelegramQaScenarioResult = LiveTransportCheckResult; @@ -157,6 +158,15 @@ type TelegramQaRunResult = { scenarios: TelegramQaScenarioResult[]; }; +type TelegramChannelStatus = { + connected?: boolean; + lastConnectedAt?: number; + lastDisconnect?: unknown; + lastError?: string | null; + restartPending?: boolean; + running?: boolean; +}; + class TelegramQaCanaryError extends Error { phase: TelegramQaCanaryPhase; context: Record; @@ -617,6 +627,14 @@ function resolveTelegramQaScenarioTimeoutMs( return parsePositiveTelegramQaEnvMs(env, "OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS", fallbackMs); } +function resolveTelegramQaReadyTimeoutMs(env: NodeJS.ProcessEnv = process.env) { + const raw = env.OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS; + if (!raw) { + return TELEGRAM_QA_DEFAULT_READY_TIMEOUT_MS; + } + return parseStrictPositiveInteger(raw) ?? TELEGRAM_QA_DEFAULT_READY_TIMEOUT_MS; +} + function normalizeTelegramQaRttOptions(params: { count?: number; checkIds?: readonly string[]; @@ -1230,13 +1248,16 @@ async function waitForTelegramChannelRunning( gateway: Awaited>, accountId: string, options?: { + env?: NodeJS.ProcessEnv; pollMs?: number; timeoutMs?: number; }, ) { const startedAt = Date.now(); - const timeoutMs = options?.timeoutMs ?? 45_000; + const timeoutMs = options?.timeoutMs ?? resolveTelegramQaReadyTimeoutMs(options?.env); const pollMs = options?.pollMs ?? 500; + let lastProbeError: string | undefined; + let lastStatus: TelegramChannelStatus | undefined; while (Date.now() - startedAt < timeoutMs) { try { const payload = (await gateway.call( @@ -1249,6 +1270,9 @@ async function waitForTelegramChannelRunning( Array<{ accountId?: string; connected?: boolean; + lastConnectedAt?: number; + lastDisconnect?: unknown; + lastError?: string | null; running?: boolean; restartPending?: boolean; }> @@ -1256,21 +1280,39 @@ async function waitForTelegramChannelRunning( }; const accounts = payload.channelAccounts?.telegram ?? []; const match = accounts.find((entry) => entry.accountId === accountId); + lastProbeError = undefined; + lastStatus = match + ? { + connected: match.connected, + lastConnectedAt: match.lastConnectedAt, + lastDisconnect: match.lastDisconnect, + lastError: match.lastError, + restartPending: match.restartPending, + running: match.running, + } + : undefined; if (match?.running && match.connected === true && match.restartPending !== true) { return; } - } catch { + } catch (error) { + lastProbeError = formatErrorMessage(error); // retry } await new Promise((resolve) => { setTimeout(resolve, pollMs); }); } - throw new Error(`telegram account "${accountId}" did not become ready`); + const details = lastStatus + ? `; last status: ${JSON.stringify(lastStatus)}` + : lastProbeError + ? `; last probe error: ${lastProbeError}` + : ""; + throw new Error(`telegram account "${accountId}" did not become ready${details}`); } async function setTelegramQaDriverGroupAuthorization(params: { driverBotId: number; + env: NodeJS.ProcessEnv; gateway: Awaited>; groupId: string; sutAccountId: string; @@ -1293,7 +1335,7 @@ async function setTelegramQaDriverGroupAuthorization(params: { mode: 0o600, }); }); - await waitForTelegramChannelRunning(params.gateway, params.sutAccountId); + await waitForTelegramChannelRunning(params.gateway, params.sutAccountId, { env: params.env }); } function renderTelegramQaMarkdown(params: { @@ -1903,7 +1945,7 @@ export async function runTelegramQaLive(params: { }), }); try { - await waitForTelegramChannelRunning(gatewayHarness.gateway, sutAccountId); + await waitForTelegramChannelRunning(gatewayHarness.gateway, sutAccountId, { env }); assertLeaseHealthy(); let latestSutMessageId: number | undefined; try { @@ -1982,6 +2024,7 @@ export async function runTelegramQaLive(params: { if (step.driverGroupAuthorization) { await setTelegramQaDriverGroupAuthorization({ driverBotId: driverIdentity.id, + env, gateway: gatewayHarness.gateway, groupId: runtimeEnv.groupId, sutAccountId, @@ -2259,6 +2302,7 @@ export const testing = { parseTelegramQaCredentialPayload, normalizeTelegramQaRttOptions, resolveTelegramQaCanaryTimeoutMs, + resolveTelegramQaReadyTimeoutMs, resolveTelegramQaScenarioTimeoutMs, resolveTelegramQaRuntimeEnv, sanitizeTelegramQaProgressValue,