-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(onboard): skip unreachable Telegram during non-interactive setup #4307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+236
−61
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
82b36f5
fix(onboard): skip unreachable Telegram during non-interactive setup
rluo8 bb96a3b
test(onboard): restore process.exit spy in finally to avoid mock leak
rluo8 4248d3e
Merge branch 'main' into fix/4238-telegram-warn-and-skip
rluo8 4e330b0
Merge branch 'main' into fix/4238-telegram-warn-and-skip
rluo8 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Unit tests for the Telegram reachability + token-validation probe. | ||
| // | ||
| // Covers the warn-and-skip behavior introduced for #4238: when api.telegram.org | ||
| // is unreachable or the bot token is rejected, onboarding should drop the | ||
| // optional Telegram integration and continue — not abort. Mirrors the Brave | ||
| // optional-component path at src/lib/onboard/web-search-flow.ts. | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import type { ProbeResult } from "./types"; | ||
|
|
||
| vi.mock("../adapters/http/probe", () => ({ | ||
| runCurlProbe: vi.fn(), | ||
| })); | ||
|
|
||
| import { runCurlProbe } from "../adapters/http/probe"; | ||
| import { checkTelegramReachability, type TelegramReachabilityDeps } from "./telegram-reachability"; | ||
|
|
||
| function probeOk(): ProbeResult { | ||
| return { ok: true, httpStatus: 200, curlStatus: 0, body: '{"ok":true}', stderr: "", message: "" }; | ||
| } | ||
| function probeHttpError(httpStatus: number): ProbeResult { | ||
| return { ok: false, httpStatus, curlStatus: 0, body: "", stderr: "", message: "" }; | ||
| } | ||
| function probeCurlError(curlStatus: number): ProbeResult { | ||
| return { ok: false, httpStatus: 0, curlStatus, body: "", stderr: "", message: "" }; | ||
| } | ||
|
|
||
| function makeDeps(overrides: Partial<TelegramReachabilityDeps> = {}): TelegramReachabilityDeps { | ||
| return { | ||
| isNonInteractive: vi.fn(() => true), | ||
| note: vi.fn(), | ||
| promptYesNoOrDefault: vi.fn(async () => true), | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| vi.mocked(runCurlProbe).mockReset(); | ||
| delete process.env.NEMOCLAW_SKIP_TELEGRAM_REACHABILITY; | ||
| }); | ||
|
|
||
| describe("checkTelegramReachability", () => { | ||
| it("returns { skipped: false } on HTTP 200 (token valid, network reachable)", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeOk()); | ||
| expect(await checkTelegramReachability("123:abc", makeDeps())).toEqual({ skipped: false }); | ||
| }); | ||
|
|
||
| it("returns { skipped: true } when curl exits 35 (TLS handshake failure)", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeCurlError(35)); | ||
| expect(await checkTelegramReachability("123:abc", makeDeps())).toEqual({ skipped: true }); | ||
| }); | ||
|
|
||
| it("returns { skipped: true } for every curl exit in TELEGRAM_NETWORK_CURL_CODES (non-interactive)", async () => { | ||
| for (const code of [6, 7, 28, 35, 52, 56]) { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeCurlError(code)); | ||
| expect( | ||
| await checkTelegramReachability("123:abc", makeDeps()), | ||
| `curlStatus=${code}`, | ||
| ).toEqual({ skipped: true }); | ||
| } | ||
| }); | ||
|
|
||
| it("returns { skipped: true } on HTTP 401 (token rejected by Telegram)", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeHttpError(401)); | ||
| expect(await checkTelegramReachability("123:abc", makeDeps())).toEqual({ skipped: true }); | ||
| }); | ||
|
|
||
| it("returns { skipped: true } on HTTP 404 (token rejected by Telegram)", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeHttpError(404)); | ||
| expect(await checkTelegramReachability("123:abc", makeDeps())).toEqual({ skipped: true }); | ||
| }); | ||
|
|
||
| it("returns { skipped: false } and skips the probe when NEMOCLAW_SKIP_TELEGRAM_REACHABILITY=1", async () => { | ||
| process.env.NEMOCLAW_SKIP_TELEGRAM_REACHABILITY = "1"; | ||
| expect(await checkTelegramReachability("123:abc", makeDeps())).toEqual({ skipped: false }); | ||
| expect(vi.mocked(runCurlProbe)).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("prompts 'Disable Telegram?' on interactive network failure and returns { skipped: true } when accepted", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeCurlError(7)); | ||
| const deps = makeDeps({ | ||
| isNonInteractive: vi.fn(() => false), | ||
| promptYesNoOrDefault: vi.fn(async () => true), | ||
| }); | ||
| const result = await checkTelegramReachability("123:abc", deps); | ||
| expect(result).toEqual({ skipped: true }); | ||
| expect(deps.promptYesNoOrDefault).toHaveBeenCalledWith( | ||
| expect.stringContaining("Disable Telegram"), | ||
| null, | ||
| true, | ||
| ); | ||
| }); | ||
|
|
||
| it("calls process.exit(1) when interactive user declines the prompt", async () => { | ||
| vi.mocked(runCurlProbe).mockReturnValue(probeCurlError(7)); | ||
| const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { | ||
| throw new Error(`__test_exit_${code ?? 0}__`); | ||
| }) as never); | ||
| try { | ||
| const deps = makeDeps({ | ||
| isNonInteractive: vi.fn(() => false), | ||
| promptYesNoOrDefault: vi.fn(async () => false), | ||
| }); | ||
| await expect(checkTelegramReachability("123:abc", deps)).rejects.toThrow("__test_exit_1__"); | ||
| expect(exitSpy).toHaveBeenCalledWith(1); | ||
| } finally { | ||
| exitSpy.mockRestore(); | ||
| } | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { runCurlProbe } from "../adapters/http/probe"; | ||
| import { cliName } from "./branding"; | ||
| import { exitOnboardFromPrompt } from "./prompt-helpers"; | ||
|
|
||
| // Curl exit codes that indicate a network-level failure (not a token problem). | ||
| // 35 (TLS handshake failure) covers corporate proxies that MITM HTTPS. | ||
| export const TELEGRAM_NETWORK_CURL_CODES = new Set([6, 7, 28, 35, 52, 56]); | ||
|
|
||
| export type TelegramReachabilityResult = { skipped: boolean }; | ||
|
|
||
| export interface TelegramReachabilityDeps { | ||
| isNonInteractive(): boolean; | ||
| note(message: string): void; | ||
| promptYesNoOrDefault( | ||
| question: string, | ||
| envVar: string | null, | ||
| defaultIsYes: boolean, | ||
| ): Promise<boolean>; | ||
| } | ||
|
|
||
| function announceTelegramSkip(reason: "unreachable" | "invalid-token"): void { | ||
| const because = | ||
| reason === "unreachable" | ||
| ? "api.telegram.org is unreachable" | ||
| : "the bot token was rejected by Telegram"; | ||
| const recovery = | ||
| reason === "unreachable" | ||
| ? "once network access is restored" | ||
| : "after setting a valid TELEGRAM_BOT_TOKEN"; | ||
| console.warn(` Telegram integration will be disabled for this onboard run because ${because}.`); | ||
| console.warn( | ||
| ` Re-run onboarding (or \`${cliName()} <name> channels add telegram\`) ${recovery}.`, | ||
| ); | ||
| } | ||
|
|
||
| export async function checkTelegramReachability( | ||
| token: string, | ||
| deps: TelegramReachabilityDeps, | ||
| ): Promise<TelegramReachabilityResult> { | ||
| if (process.env.NEMOCLAW_SKIP_TELEGRAM_REACHABILITY === "1") { | ||
| deps.note(" [non-interactive] Skipping Telegram reachability probe by request."); | ||
| return { skipped: false }; | ||
| } | ||
|
|
||
| const result = runCurlProbe([ | ||
| "-sS", | ||
| "--connect-timeout", | ||
| "5", | ||
| "--max-time", | ||
| "10", | ||
| `https://api.telegram.org/bot${token}/getMe`, | ||
| ]); | ||
|
|
||
| // HTTP 200 with "ok":true — Telegram is reachable and token is valid. | ||
| if (result.ok) return { skipped: false }; | ||
|
|
||
| // HTTP 401 or 404 — Telegram rejected the bot token. The integration cannot | ||
| // function with an invalid token, so this is "validation fails" per #4238 and | ||
| // takes the same warn-and-skip path as a network failure: drop telegram from | ||
| // the active messaging channel set instead of letting onboarding write an | ||
| // unusable token into the sandbox/provider config. | ||
| if (result.httpStatus === 401 || result.httpStatus === 404) { | ||
| console.log(""); | ||
| console.log(" ⚠ Bot token was rejected by Telegram — verify the token is correct."); | ||
| announceTelegramSkip("invalid-token"); | ||
| return { skipped: true }; | ||
| } | ||
|
|
||
| // Network-level failure — Telegram is unreachable from this host. Treat as | ||
| // an optional-integration soft-fail (#4238): warn, drop telegram from the | ||
| // active messaging channel set, and let onboarding continue. Matches the | ||
| // warn-and-skip pattern Brave uses at src/lib/onboard/web-search-flow.ts. | ||
| if (result.curlStatus && TELEGRAM_NETWORK_CURL_CODES.has(result.curlStatus)) { | ||
| console.log(""); | ||
| console.log(" ⚠ api.telegram.org is not reachable from this host."); | ||
| console.log(" Telegram integration requires outbound HTTPS access to api.telegram.org."); | ||
| console.log(" This is commonly blocked by corporate network proxies."); | ||
|
|
||
| if (deps.isNonInteractive()) { | ||
| announceTelegramSkip("unreachable"); | ||
| return { skipped: true }; | ||
| } | ||
| // Interactive: prompt explicitly asks whether to skip telegram and continue. | ||
| // Default Y favors the soft-fail path so an enter-press lines up with the | ||
| // optional-integration contract. An explicit N still aborts onboarding — | ||
| // the user opted out of both telegram and the workaround. | ||
| if ( | ||
| await deps.promptYesNoOrDefault( | ||
| " Disable Telegram for this run and continue?", | ||
| null, | ||
| true, | ||
| ) | ||
| ) { | ||
| announceTelegramSkip("unreachable"); | ||
| return { skipped: true }; | ||
| } | ||
| exitOnboardFromPrompt(); | ||
| } | ||
|
|
||
| // Unexpected probe failure — warn but don't block. | ||
| if (!result.ok && result.httpStatus > 0) { | ||
| console.log( | ||
| ` ⚠ Telegram API returned HTTP ${result.httpStatus} — the bot may not work correctly.`, | ||
| ); | ||
| } else if (!result.ok) { | ||
| console.log(` ⚠ Telegram reachability probe failed: ${result.message}`); | ||
| } | ||
| return { skipped: false }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.