diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx index 3024ad1080..eb6ac512b1 100644 --- a/packages/cli/src/ui/auth/AuthDialog.test.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx @@ -17,13 +17,11 @@ import { import { AuthDialog } from './AuthDialog.js'; import { AuthType, type Config, debugLogger } from '@google/gemini-cli-core'; import type { LoadedSettings } from '../../config/settings.js'; -import { SettingScope } from '../../config/settings.js'; import { AuthState } from '../types.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { validateAuthMethodWithSettings } from './useAuth.js'; import { runExitCleanup } from '../../utils/cleanup.js'; -import { clearCachedCredentialFile } from '@google/gemini-cli-core'; import { Text } from 'ink'; import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js'; @@ -66,7 +64,6 @@ const mockedUseKeypress = useKeypress as Mock; const mockedRadioButtonSelect = RadioButtonSelect as Mock; const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock; const mockedRunExitCleanup = runExitCleanup as Mock; -const mockedClearCachedCredentialFile = clearCachedCredentialFile as Mock; describe('AuthDialog', () => { let props: { @@ -220,24 +217,48 @@ describe('AuthDialog', () => { expect(props.settings.setValue).not.toHaveBeenCalled(); }); - it('calls onSelect if validation passes', async () => { + it('skips API key dialog on initial setup if env var is present', async () => { mockedValidateAuthMethod.mockReturnValue(null); + process.env['GEMINI_API_KEY'] = 'test-key-from-env'; + // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup + renderWithProviders(); const { onSelect: handleAuthSelect } = mockedRadioButtonSelect.mock.calls[0][0]; await handleAuthSelect(AuthType.USE_GEMINI); - expect(mockedValidateAuthMethod).toHaveBeenCalledWith( - AuthType.USE_GEMINI, - props.settings, + expect(props.setAuthState).toHaveBeenCalledWith( + AuthState.Unauthenticated, ); - expect(props.onAuthError).not.toHaveBeenCalled(); - expect(mockedClearCachedCredentialFile).toHaveBeenCalled(); - expect(props.settings.setValue).toHaveBeenCalledWith( - SettingScope.User, - 'security.auth.selectedType', - AuthType.USE_GEMINI, + }); + + it('shows API key dialog on initial setup if no env var is present', async () => { + mockedValidateAuthMethod.mockReturnValue(null); + // process.env['GEMINI_API_KEY'] is not set + // props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup + + renderWithProviders(); + const { onSelect: handleAuthSelect } = + mockedRadioButtonSelect.mock.calls[0][0]; + await handleAuthSelect(AuthType.USE_GEMINI); + + expect(props.setAuthState).toHaveBeenCalledWith( + AuthState.AwaitingApiKeyInput, ); + }); + + it('shows API key dialog on re-auth to allow editing', async () => { + mockedValidateAuthMethod.mockReturnValue(null); + process.env['GEMINI_API_KEY'] = 'test-key-from-env'; + // Simulate that the user has already authenticated once + props.settings.merged.security!.auth!.selectedType = + AuthType.LOGIN_WITH_GOOGLE; + + renderWithProviders(); + const { onSelect: handleAuthSelect } = + mockedRadioButtonSelect.mock.calls[0][0]; + await handleAuthSelect(AuthType.USE_GEMINI); + expect(props.setAuthState).toHaveBeenCalledWith( AuthState.AwaitingApiKeyInput, ); diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index da5b6d7dff..7433501304 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -116,6 +116,9 @@ export function AuthDialog({ return; } if (authType) { + const isInitialAuthSelection = + !settings.merged.security?.auth?.selectedType; + await clearCachedCredentialFile(); settings.setValue(scope, 'security.auth.selectedType', authType); @@ -130,10 +133,16 @@ export function AuthDialog({ }, 100); return; } - } - if (authType === AuthType.USE_GEMINI) { - setAuthState(AuthState.AwaitingApiKeyInput); - return; + + if (authType === AuthType.USE_GEMINI) { + if (isInitialAuthSelection && process.env['GEMINI_API_KEY']) { + setAuthState(AuthState.Unauthenticated); + return; + } else { + setAuthState(AuthState.AwaitingApiKeyInput); + return; + } + } } setAuthState(AuthState.Unauthenticated); }, diff --git a/packages/cli/src/ui/auth/useAuth.test.tsx b/packages/cli/src/ui/auth/useAuth.test.tsx index 266eda2676..89560e58f7 100644 --- a/packages/cli/src/ui/auth/useAuth.test.tsx +++ b/packages/cli/src/ui/auth/useAuth.test.tsx @@ -214,6 +214,23 @@ describe('useAuth', () => { }); }); + it('should prioritize env key over stored key when both are present', async () => { + mockLoadApiKey.mockResolvedValue('stored-key'); + process.env['GEMINI_API_KEY'] = 'env-key'; + const { result } = renderHook(() => + useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig), + ); + + await waitFor(() => { + expect(mockConfig.refreshAuth).toHaveBeenCalledWith( + AuthType.USE_GEMINI, + ); + expect(result.current.authState).toBe(AuthState.Authenticated); + // The environment key should take precedence + expect(result.current.apiKeyDefaultValue).toBe('env-key'); + }); + }); + it('should set error if validation fails', async () => { mockValidateAuthMethod.mockReturnValue('Validation Failed'); const { result } = renderHook(() => diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts index 336fe3efbe..f57c727e25 100644 --- a/packages/cli/src/ui/auth/useAuth.ts +++ b/packages/cli/src/ui/auth/useAuth.ts @@ -57,11 +57,17 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => { const reloadApiKey = useCallback(async () => { const storedKey = (await loadApiKey()) ?? ''; const envKey = process.env['GEMINI_API_KEY'] ?? ''; - const key = storedKey || envKey; + const key = envKey || storedKey; setApiKeyDefaultValue(key); return key; // Return the key for immediate use }, []); + useEffect(() => { + if (authState === AuthState.AwaitingApiKeyInput) { + reloadApiKey(); + } + }, [authState, reloadApiKey]); + useEffect(() => { (async () => { if (authState !== AuthState.Unauthenticated) {