From 348d93cd65b041a3b0968b8e9d8ee2ee2e4aa3e9 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:08:34 +0200 Subject: [PATCH 01/12] fix(auth): surface .sentryclirc source in self-hosted login errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When auth login rejects a non-SaaS URL that arrived via the .sentryclirc shim, the error now names the file that provided the URL and gives the exact command to fix it, rather than a generic "--url was not provided" message. Also shows a one-line tip when the user runs auth login without --token but .sentryclirc already has a token — pointing them at the faster token path instead of silently starting the OAuth device flow. Closes #975 --- src/commands/auth/login.ts | 83 +++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index b996d999f..cf1c86014 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -42,6 +42,7 @@ import { normalizeOrigin, normalizeUserInputToOrigin, } from "../../lib/sentry-urls.js"; +import { loadSentryCliRc } from "../../lib/sentryclirc.js"; import { isLoginTrustAnchorFor, registerLoginTrustAnchor, @@ -115,10 +116,15 @@ export function parseLoginUrl(raw: string): string { * a trusted source (`--url` flag or boot-time env snapshot), so "no * matching anchor" is the load-bearing signal that the host arrived via * an untrusted channel. + * + * @param rcSource - Path of the `.sentryclirc` file that provided the URL, + * if that's where the host came from. Used to produce a more actionable + * error message pointing at the specific file. */ function refuseLoginToUntrustedHost( flags: LoginFlags, - effectiveHost: string + effectiveHost: string, + rcSource?: string ): void { if ( flags.url || @@ -127,11 +133,62 @@ function refuseLoginToUntrustedHost( ) { return; } - const tokenHint = flags.token ? " --token " : ""; + const tokenFlag = flags.token ? ` --token ${flags.token.slice(0, 8)}…` : ""; + const sourceClause = rcSource + ? `this URL was read from .sentryclirc (${rcSource}) but hasn't been confirmed as trusted yet` + : "--url was not provided"; throw new HostScopeError( - `Refusing to log in against ${effectiveHost} without explicit --url.\n` + - "Pass the host explicitly to confirm you trust it:\n" + - ` sentry auth login --url ${effectiveHost}${tokenHint}` + `Refusing to log in against ${effectiveHost} — ${sourceClause}.\n\n` + + "To authenticate against this self-hosted instance, confirm the host explicitly:\n" + + ` sentry auth login --url ${effectiveHost}${tokenFlag}` + ); +} + +/** + * Resolve which `.sentryclirc` file (if any) provided the effective host, and + * return its path alongside the full rc config for downstream use. + * + * Separating this into its own function keeps {@link loginCommand}'s `func` + * complexity within limits. + */ +async function resolveRcContext( + flagUrl: string | undefined, + cwd: string, + effectiveHost: string +): Promise<{ + rcConfig: Awaited>; + urlFromRc: string | undefined; +}> { + const rcConfig = await loadSentryCliRc(cwd); + const rcUrlNormalized = rcConfig.url + ? normalizeOrigin(normalizeUrl(rcConfig.url)) + : undefined; + const urlFromRc = + !flagUrl && + !!rcUrlNormalized && + normalizeOrigin(effectiveHost) === rcUrlNormalized + ? rcConfig.sources.url + : undefined; + return { rcConfig, urlFromRc }; +} + +/** + * When the user is about to start an OAuth flow but `.sentryclirc` already + * has a token, surface the faster `--token` path as a one-line tip. + */ +function maybeWarnRcToken( + rcConfig: Awaited>, + urlFromRc: string | undefined, + effectiveHost: string +): void { + if (!rcConfig.token) { + return; + } + const masked = `${rcConfig.token.slice(0, 8)}…`; + const urlHint = urlFromRc ? ` --url ${effectiveHost}` : ""; + log.info( + `Tip: Found a token in .sentryclirc (${rcConfig.sources.token}). ` + + `To use it: sentry auth login --token ${masked}${urlHint}` ); } @@ -287,7 +344,16 @@ export const loginCommand = buildCommand({ // requested instance. Default URL persistence is deferred until login // succeeds — see persistLoginUrlAsDefault calls below. const effectiveHost = applyLoginUrl(flags.url); - refuseLoginToUntrustedHost(flags, effectiveHost); + + // Check whether the effective URL came from .sentryclirc so we can name + // the source file in trust-refusal errors and show a migration tip. + const { rcConfig, urlFromRc } = await resolveRcContext( + flags.url, + process.cwd(), + effectiveHost + ); + + refuseLoginToUntrustedHost(flags, effectiveHost, urlFromRc); // Check if already authenticated and handle re-authentication if (isAuthenticated()) { @@ -297,6 +363,11 @@ export const loginCommand = buildCommand({ } } + // If going through the OAuth flow but .sentryclirc has a token, tip the user. + if (!flags.token) { + maybeWarnRcToken(rcConfig, urlFromRc, effectiveHost); + } + // Clear stale cached responses from a previous session try { await clearResponseCache(); From afb40272c42aed27a6884f82b51bad0803778181 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:20:53 +0200 Subject: [PATCH 02/12] fix(auth): remove partial token from error and hint messages Partial tokens in error messages and log output can end up in CI logs, terminal recordings, and bug reports. Use placeholder strings instead. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index cf1c86014..45f42fbe9 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -133,7 +133,7 @@ function refuseLoginToUntrustedHost( ) { return; } - const tokenFlag = flags.token ? ` --token ${flags.token.slice(0, 8)}…` : ""; + const tokenFlag = flags.token ? " --token " : ""; const sourceClause = rcSource ? `this URL was read from .sentryclirc (${rcSource}) but hasn't been confirmed as trusted yet` : "--url was not provided"; @@ -184,7 +184,7 @@ function maybeWarnRcToken( if (!rcConfig.token) { return; } - const masked = `${rcConfig.token.slice(0, 8)}…`; + const masked = ""; const urlHint = urlFromRc ? ` --url ${effectiveHost}` : ""; log.info( `Tip: Found a token in .sentryclirc (${rcConfig.sources.token}). ` + From 45fe6cdbbabb0d629d8d6904ee5120d5b7784cdf Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:26:36 +0200 Subject: [PATCH 03/12] refactor(auth): surface rc token tip as footer hint instead of log.info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit log.info fires before the OAuth flow starts — wrong timing and wrong visual weight. Return the tip as a hint instead so it appears as a muted footer after login completes, consistent with how every other command surfaces follow-up suggestions. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 45f42fbe9..a29cc455c 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -173,22 +173,22 @@ async function resolveRcContext( } /** - * When the user is about to start an OAuth flow but `.sentryclirc` already - * has a token, surface the faster `--token` path as a one-line tip. + * Returns a hint string when .sentryclirc contains a token the user could + * pass directly via --token instead of going through the OAuth flow. + * Returned as a footer hint so it appears after login completes, not before. */ -function maybeWarnRcToken( +function rcTokenHint( rcConfig: Awaited>, urlFromRc: string | undefined, effectiveHost: string -): void { +): string | undefined { if (!rcConfig.token) { return; } - const masked = ""; const urlHint = urlFromRc ? ` --url ${effectiveHost}` : ""; - log.info( - `Tip: Found a token in .sentryclirc (${rcConfig.sources.token}). ` + - `To use it: sentry auth login --token ${masked}${urlHint}` + return ( + `Found a token in .sentryclirc (${rcConfig.sources.token}). ` + + `To skip OAuth next time: sentry auth login --token ${urlHint}` ); } @@ -363,11 +363,6 @@ export const loginCommand = buildCommand({ } } - // If going through the OAuth flow but .sentryclirc has a token, tip the user. - if (!flags.token) { - maybeWarnRcToken(rcConfig, urlFromRc, effectiveHost); - } - // Clear stale cached responses from a previous session try { await clearResponseCache(); @@ -434,10 +429,10 @@ export const loginCommand = buildCommand({ // Fire-and-forget — login already succeeded, caching is best-effort. warmOrgCache(); yield new CommandOutput(result); - } else { - // Error already displayed by runInteractiveLogin - process.exitCode = 1; + return { hint: rcTokenHint(rcConfig, urlFromRc, effectiveHost) }; } + // Error already displayed by runInteractiveLogin + process.exitCode = 1; }, }); From 0d432dcabb02330cafcb99bd2c73b2475b4e0fe9 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:28:06 +0200 Subject: [PATCH 04/12] docs(auth): remove implementation note from resolveRcContext jsdoc Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index a29cc455c..36d31b92b 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -147,9 +147,6 @@ function refuseLoginToUntrustedHost( /** * Resolve which `.sentryclirc` file (if any) provided the effective host, and * return its path alongside the full rc config for downstream use. - * - * Separating this into its own function keeps {@link loginCommand}'s `func` - * complexity within limits. */ async function resolveRcContext( flagUrl: string | undefined, From bba7e4bebe0a4107fba0cc91b4fe3690efbf3ff6 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:29:29 +0200 Subject: [PATCH 05/12] refactor(auth): use SentryCliRcConfig instead of inline Awaited Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 36d31b92b..10a6d0f35 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -42,7 +42,10 @@ import { normalizeOrigin, normalizeUserInputToOrigin, } from "../../lib/sentry-urls.js"; -import { loadSentryCliRc } from "../../lib/sentryclirc.js"; +import { + loadSentryCliRc, + type SentryCliRcConfig, +} from "../../lib/sentryclirc.js"; import { isLoginTrustAnchorFor, registerLoginTrustAnchor, @@ -153,7 +156,7 @@ async function resolveRcContext( cwd: string, effectiveHost: string ): Promise<{ - rcConfig: Awaited>; + rcConfig: SentryCliRcConfig; urlFromRc: string | undefined; }> { const rcConfig = await loadSentryCliRc(cwd); @@ -175,7 +178,7 @@ async function resolveRcContext( * Returned as a footer hint so it appears after login completes, not before. */ function rcTokenHint( - rcConfig: Awaited>, + rcConfig: SentryCliRcConfig, urlFromRc: string | undefined, effectiveHost: string ): string | undefined { From ca1ca72a41a7d275e28d0e726d4e35e8ada13777 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 18:31:56 +0200 Subject: [PATCH 06/12] refactor(auth): remove what-comments, keep only why-comments Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 10a6d0f35..8f27c1059 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -355,7 +355,6 @@ export const loginCommand = buildCommand({ refuseLoginToUntrustedHost(flags, effectiveHost, urlFromRc); - // Check if already authenticated and handle re-authentication if (isAuthenticated()) { const shouldProceed = await handleExistingAuth(flags.force); if (!shouldProceed) { @@ -363,25 +362,21 @@ export const loginCommand = buildCommand({ } } - // Clear stale cached responses from a previous session try { await clearResponseCache(); } catch { // Non-fatal: cache directory may not exist } - // Token-based authentication if (flags.token) { // Save token first (with host scope), then validate by fetching user regions await setAuthToken(flags.token, undefined, undefined, { host: effectiveHost, }); - // Validate token by fetching user regions try { await getUserRegions(); } catch { - // Token is invalid - clear it and throw await clearAuth(); throw new AuthError( "invalid", @@ -389,7 +384,6 @@ export const loginCommand = buildCommand({ ); } - // Login succeeded — persist default URL for subsequent invocations. persistLoginUrlAsDefault(flags.url, effectiveHost); // Fetch and cache user info via /auth/ (works with all token types). @@ -423,10 +417,7 @@ export const loginCommand = buildCommand({ }); if (result) { - // Login succeeded — persist default URL for subsequent invocations. persistLoginUrlAsDefault(flags.url, effectiveHost); - // Warm the org + region cache so the first real command is fast. - // Fire-and-forget — login already succeeded, caching is best-effort. warmOrgCache(); yield new CommandOutput(result); return { hint: rcTokenHint(rcConfig, urlFromRc, effectiveHost) }; From 52c14dd36d5b311b5360819937f84cfe02f243ef Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:02:11 +0200 Subject: [PATCH 07/12] fix(auth): include --url in rc token hint for self-hosted instances urlFromRc is undefined when --url is passed explicitly, so gating on it silently dropped --url from the hint, pointing users at SaaS. Gate on !isSaaSTrustOrigin(effectiveHost) instead so the hint is always correct regardless of how the host was supplied. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 8f27c1059..f97ca3ffd 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -179,13 +179,16 @@ async function resolveRcContext( */ function rcTokenHint( rcConfig: SentryCliRcConfig, - urlFromRc: string | undefined, effectiveHost: string ): string | undefined { if (!rcConfig.token) { return; } - const urlHint = urlFromRc ? ` --url ${effectiveHost}` : ""; + // Always include --url for self-hosted instances regardless of how the host + // was supplied — omitting it would point the user at SaaS instead. + const urlHint = isSaaSTrustOrigin(effectiveHost) + ? "" + : ` --url ${effectiveHost}`; return ( `Found a token in .sentryclirc (${rcConfig.sources.token}). ` + `To skip OAuth next time: sentry auth login --token ${urlHint}` @@ -420,7 +423,7 @@ export const loginCommand = buildCommand({ persistLoginUrlAsDefault(flags.url, effectiveHost); warmOrgCache(); yield new CommandOutput(result); - return { hint: rcTokenHint(rcConfig, urlFromRc, effectiveHost) }; + return { hint: rcTokenHint(rcConfig, effectiveHost) }; } // Error already displayed by runInteractiveLogin process.exitCode = 1; From bfed8bf9d5dd3edfa735cd449bff356b8497d690 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:08:36 +0200 Subject: [PATCH 08/12] fix(auth): suppress rc token hint when token is for a different host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If .sentryclirc has a URL that doesn't match effectiveHost the stored token belongs to a different instance. Showing the hint would suggest using it for the wrong host, which would always fail with an auth error. No API call needed — the host mismatch is detectable from rcConfig.url. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index f97ca3ffd..a82eed5c3 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -176,6 +176,10 @@ async function resolveRcContext( * Returns a hint string when .sentryclirc contains a token the user could * pass directly via --token instead of going through the OAuth flow. * Returned as a footer hint so it appears after login completes, not before. + * + * Only shown when the stored token is plausibly for the current host: either + * no URL is set in the rc file (global SaaS token) or the rc URL matches + * effectiveHost. A mismatched URL means the token is for a different instance. */ function rcTokenHint( rcConfig: SentryCliRcConfig, @@ -184,6 +188,12 @@ function rcTokenHint( if (!rcConfig.token) { return; } + const rcUrl = rcConfig.url + ? normalizeOrigin(normalizeUrl(rcConfig.url)) + : undefined; + if (rcUrl && rcUrl !== normalizeOrigin(effectiveHost)) { + return; + } // Always include --url for self-hosted instances regardless of how the host // was supplied — omitting it would point the user at SaaS instead. const urlHint = isSaaSTrustOrigin(effectiveHost) From 19b2357bca7a1c2ee9a12dfdd52142af6ab98eab Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:14:58 +0200 Subject: [PATCH 09/12] fix(auth): use this.cwd and tighten rc token hint host matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: - resolveRcContext now uses this.cwd (injected via SentryContext) instead of process.cwd(), making rc-related login behavior testable - rcTokenHint now suppresses the hint when .sentryclirc has no URL and effectiveHost is self-hosted — a bare token in rc is almost certainly a SaaS token and pairing it with a self-hosted --url would always fail Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index a82eed5c3..57a84ae90 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -191,8 +191,12 @@ function rcTokenHint( const rcUrl = rcConfig.url ? normalizeOrigin(normalizeUrl(rcConfig.url)) : undefined; - if (rcUrl && rcUrl !== normalizeOrigin(effectiveHost)) { - return; + if (rcUrl) { + // rc has an explicit URL — only hint if it matches the current host + if (rcUrl !== normalizeOrigin(effectiveHost)) return; + } else { + // No URL in rc — assume a bare SaaS token; don't hint on self-hosted + if (!isSaaSTrustOrigin(effectiveHost)) return; } // Always include --url for self-hosted instances regardless of how the host // was supplied — omitting it would point the user at SaaS instead. @@ -362,7 +366,7 @@ export const loginCommand = buildCommand({ // the source file in trust-refusal errors and show a migration tip. const { rcConfig, urlFromRc } = await resolveRcContext( flags.url, - process.cwd(), + this.cwd, effectiveHost ); From 472df59ebd2a298b936ca96a10b24ab3d9e436c0 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:16:06 +0200 Subject: [PATCH 10/12] fix(auth): fix lint errors in rc token hint host matching Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 57a84ae90..93a43476a 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -191,12 +191,13 @@ function rcTokenHint( const rcUrl = rcConfig.url ? normalizeOrigin(normalizeUrl(rcConfig.url)) : undefined; - if (rcUrl) { - // rc has an explicit URL — only hint if it matches the current host - if (rcUrl !== normalizeOrigin(effectiveHost)) return; - } else { - // No URL in rc — assume a bare SaaS token; don't hint on self-hosted - if (!isSaaSTrustOrigin(effectiveHost)) return; + // Token is for a different host — don't suggest it + if (rcUrl && rcUrl !== normalizeOrigin(effectiveHost)) { + return; + } + // No URL in rc means a bare SaaS token — don't suggest it for self-hosted + if (!(rcUrl || isSaaSTrustOrigin(effectiveHost))) { + return; } // Always include --url for self-hosted instances regardless of how the host // was supplied — omitting it would point the user at SaaS instead. From 9aee1263f51884cb59532a5b43513a20fc8b3313 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:21:02 +0200 Subject: [PATCH 11/12] test(auth): add unit tests for rcTokenHint host-matching logic Covers the five branches that were implicated in review bugs: no token, SaaS match, self-hosted rc URL match, rc URL mismatch, and bare SaaS token against a self-hosted host. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/commands/auth/login.ts | 3 +- test/commands/auth/login.test.ts | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 93a43476a..d277740db 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -181,7 +181,8 @@ async function resolveRcContext( * no URL is set in the rc file (global SaaS token) or the rc URL matches * effectiveHost. A mismatched URL means the token is for a different instance. */ -function rcTokenHint( +/** @internal exported for testing */ +export function rcTokenHint( rcConfig: SentryCliRcConfig, effectiveHost: string ): string | undefined { diff --git a/test/commands/auth/login.test.ts b/test/commands/auth/login.test.ts index a567be8ee..fd4a116c8 100644 --- a/test/commands/auth/login.test.ts +++ b/test/commands/auth/login.test.ts @@ -79,6 +79,7 @@ mock.module("../../../src/lib/logger.js", () => ({ // Dynamic import: must run AFTER mock.module() so login.ts picks up fakeLog. const { loginCommand } = await import("../../../src/commands/auth/login.js"); +import { rcTokenHint } from "../../../src/commands/auth/login.js"; // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking import * as apiClient from "../../../src/lib/api-client.js"; // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking @@ -88,6 +89,7 @@ import * as dbUser from "../../../src/lib/db/user.js"; import { AuthError } from "../../../src/lib/errors.js"; // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking import * as interactiveLogin from "../../../src/lib/interactive-login.js"; +import type { SentryCliRcConfig } from "../../../src/lib/sentryclirc.js"; type LoginFlags = { readonly token?: string; @@ -780,3 +782,52 @@ describe("applyLoginUrl (trust anchor registration)", () => { ).toBe(false); }); }); + +function makeRcConfig( + token: string | undefined, + url?: string +): SentryCliRcConfig { + return { + token, + url, + sources: { token: token ? "~/.sentryclirc" : undefined }, + }; +} + +describe("rcTokenHint", () => { + test("no token → no hint", () => { + expect( + rcTokenHint(makeRcConfig(undefined), "https://sentry.io") + ).toBeUndefined(); + }); + + test("SaaS host, no rc URL → hint without --url", () => { + const hint = rcTokenHint(makeRcConfig("sntrys_abc"), "https://sentry.io"); + expect(hint).toContain("sentry auth login --token "); + expect(hint).not.toContain("--url"); + }); + + test("self-hosted, rc URL matches → hint includes --url", () => { + const hint = rcTokenHint( + makeRcConfig("sntrys_abc", "https://self.example.com"), + "https://self.example.com" + ); + expect(hint).toContain("--url https://self.example.com"); + }); + + test("self-hosted, rc URL mismatches → no hint (token is for a different instance)", () => { + const hint = rcTokenHint( + makeRcConfig("sntrys_abc", "https://other.example.com"), + "https://self.example.com" + ); + expect(hint).toBeUndefined(); + }); + + test("self-hosted, no rc URL → no hint (bare SaaS token shouldn't be suggested for self-hosted)", () => { + const hint = rcTokenHint( + makeRcConfig("sntrys_abc"), + "https://self.example.com" + ); + expect(hint).toBeUndefined(); + }); +}); From b65f4bea6fbadbc7ed1accccef55de84d50c97ac Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 18 May 2026 22:28:39 +0200 Subject: [PATCH 12/12] fix(test): use dynamic import for rcTokenHint to preserve logger mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static imports are hoisted and evaluate before mock.module() runs, so login.ts would bind the real consola logger instead of fakeLog — breaking the interactive prompt tests. Pull rcTokenHint into the existing dynamic import block that was already there for this reason. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- test/commands/auth/login.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/commands/auth/login.test.ts b/test/commands/auth/login.test.ts index fd4a116c8..d46a314ff 100644 --- a/test/commands/auth/login.test.ts +++ b/test/commands/auth/login.test.ts @@ -77,9 +77,10 @@ mock.module("../../../src/lib/logger.js", () => ({ })); // Dynamic import: must run AFTER mock.module() so login.ts picks up fakeLog. -const { loginCommand } = await import("../../../src/commands/auth/login.js"); +const { loginCommand, rcTokenHint } = await import( + "../../../src/commands/auth/login.js" +); -import { rcTokenHint } from "../../../src/commands/auth/login.js"; // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking import * as apiClient from "../../../src/lib/api-client.js"; // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking