diff --git a/src/__tests__/bugfixes.test.ts b/src/__tests__/bugfixes.test.ts index 3177b7d868..1a7e95d54d 100644 --- a/src/__tests__/bugfixes.test.ts +++ b/src/__tests__/bugfixes.test.ts @@ -695,3 +695,55 @@ describe('Dev-channels dialog coverage', () => { ) }) }) + +// --------------------------------------------------------------------------- +// Fix: onboarding + trust dialog skipped entirely for third-party providers +// --------------------------------------------------------------------------- +// Behavioral coverage lives in src/utils/setupScreenGates.test.ts — the +// gating decisions were extracted into that provider-free seam because this +// module's import chain cannot be loaded under bun test (compile-time +// feature() macro checker, same constraint as the dev-channels tests above). +// These wiring checks assert showSetupScreens actually consults the seam and +// that no provider gate was re-introduced around the dialogs. +describe('Onboarding and trust dialog — third-party providers', () => { + test('showSetupScreens routes both dialogs through the provider-free seam', async () => { + const content = await file('interactiveHelpers.tsx').text() + + expect(content).toContain('getRequiredSetupScreens({') + expect(content).toContain('if (setupScreens.onboarding)') + expect(content).toContain('if (setupScreens.trustDialog)') + }) + + test('the env-config option never renders the raw endpoint', async () => { + // OPENAI_BASE_URL/OPENAI_API_BASE can carry credentials (userinfo or + // token query params) and everything rendered lands in terminal + // scrollback. Redaction behavior is tested in envProviderOption.test.ts; + // this guards the wiring — the raw `envBaseUrl` may only reach profile + // persistence (addProviderProfile/getProviderProfiles/label), never a + // rendered label or status message. + const content = await file('components/ConsoleOAuthFlow.tsx').text() + + expect(content).toContain('getEnvProviderOption()') + // Rendered sites use the redacted value. + expect(content).toMatch(/\{envBaseUrlVarName\}=\{envBaseUrlForDisplay\}/) + expect(content).toMatch(/\$\{envBaseUrlForDisplay\}\) as your active provider/) + // No rendered site interpolates the raw endpoint. + expect(content).not.toMatch(/\{envBaseUrl\}/) + expect(content).not.toMatch(/\$\{envBaseUrl\}/) + }) + + test('no dialog is gated behind usesAnthropicSetup', async () => { + const content = await file('interactiveHelpers.tsx').text() + + // Theme choice + security notes are universal, and workspace trust is + // orthogonal to the API provider: an untrusted repo is exactly as + // dangerous over a local model as over Anthropic. The seam takes no + // provider input, so the only way to regress is to add a gate at the + // call sites — which this guards against. + expect(content).not.toMatch(/usesAnthropicSetup\s*&&\s*\(?\s*setupScreens/) + expect(content).not.toMatch(/usesAnthropicSetup\s*&&\s*\(\s*!config\.theme/) + expect(content).not.toMatch( + /usesAnthropicSetup\s*&&\s*!checkHasTrustDialogAccepted/, + ) + }) +}) diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 65a06ce4e6..d66e91ca63 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -11,6 +11,15 @@ import { sendNotification } from '../services/notifier.js'; import { OAuthService } from '../services/oauth/index.js'; import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; import { logError } from '../utils/log.js'; +import { getEnvProviderOption } from '../utils/envProviderOption.js'; +import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js'; +import { type ProviderProfile } from '../utils/config.js'; +import { + addProviderProfile, + getProviderProfiles, + setActiveProviderProfile, + updateProviderProfile, +} from '../utils/providerProfiles.js'; import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; import { ProviderManager } from './ProviderManager.js'; import { Select } from './CustomSelect/select.js'; @@ -386,7 +395,42 @@ function OAuthStatusMessage({ startingMessage || 'OpenClaude can be used with your Claude subscription or billed based on API usage through your Console account.' + // OPENAI_BASE_URL/OPENAI_MODEL in the environment signal an + // OpenAI-compatible setup the user already has — offer to adopt it as + // the active provider profile instead of walking them through login for + // an account they may never have wanted. Env vars alone do NOT activate + // the route (resolveActiveRouteIdFromEnv requires CLAUDE_CODE_USE_OPENAI + // or a saved profile), so selecting this saves + activates a profile. + // Both fields gate the option because a profile requires baseUrl+model. + // getEnvProviderOption owns the secret-disclosure boundary: only + // `displayBaseUrl` (redacted) may be rendered — the raw `baseUrl` + // exists solely for profile creation/activation. See its tests for + // the credential cases. + const { + available: envConfigAvailable, + varName: envBaseUrlVarName, + baseUrl: envBaseUrl, + displayBaseUrl: envBaseUrlForDisplay, + model: envModel, + } = getEnvProviderOption() + const loginOptions = [ + ...(envConfigAvailable + ? [ + { + label: ( + + Use current environment configuration ·{' '} + + {envBaseUrlVarName}={envBaseUrlForDisplay} + + {'\n'} + + ), + value: 'environment' as const, + }, + ] + : []), { label: ( @@ -427,6 +471,68 @@ function OAuthStatusMessage({