From 1f70a27e9cbd2a10119cdf90ae6ba933bdcc08ae Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Sat, 13 Sep 2025 08:18:40 +0900 Subject: [PATCH] JSON errors in non-interactive auth validation (#8373) --- integration-tests/json-output.test.ts | 34 +++++ .../src/validateNonInterActiveAuth.test.ts | 118 +++++++++++++++++- .../cli/src/validateNonInterActiveAuth.ts | 67 +++++----- 3 files changed, 190 insertions(+), 29 deletions(-) diff --git a/integration-tests/json-output.test.ts b/integration-tests/json-output.test.ts index 27caee4003..a998d4a731 100644 --- a/integration-tests/json-output.test.ts +++ b/integration-tests/json-output.test.ts @@ -34,4 +34,38 @@ describe('JSON output', () => { expect(parsed).toHaveProperty('stats'); expect(typeof parsed.stats).toBe('object'); }); + + it('should return a JSON error for enforced auth mismatch before running', async () => { + process.env['GOOGLE_GENAI_USE_GCA'] = 'true'; + await rig.setup('json-output-auth-mismatch', { + settings: { + security: { auth: { enforcedType: 'gemini-api-key' } }, + }, + }); + + let thrown: Error | undefined; + try { + await rig.run('Hello', '--output-format', 'json'); + expect.fail('Expected process to exit with error'); + } catch (e) { + thrown = e as Error; + } finally { + delete process.env['GOOGLE_GENAI_USE_GCA']; + } + + expect(thrown).toBeDefined(); + const message = (thrown as Error).message; + const jsonStart = message.indexOf('{'); + expect(jsonStart).toBeGreaterThan(-1); + const payload = JSON.parse(message.slice(jsonStart)); + expect(payload.error).toBeDefined(); + expect(payload.error.type).toBe('Error'); + expect(payload.error.code).toBe(1); + expect(payload.error.message).toContain( + 'configured auth type is gemini-api-key', + ); + expect(payload.error.message).toContain( + 'current auth type is oauth-personal', + ); + }); }); diff --git a/packages/cli/src/validateNonInterActiveAuth.test.ts b/packages/cli/src/validateNonInterActiveAuth.test.ts index f7ef8c85be..9f3159c88a 100644 --- a/packages/cli/src/validateNonInterActiveAuth.test.ts +++ b/packages/cli/src/validateNonInterActiveAuth.test.ts @@ -6,7 +6,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; -import { AuthType } from '@google/gemini-cli-core'; +import { AuthType, OutputFormat } from '@google/gemini-cli-core'; +import type { Config } from '@google/gemini-cli-core'; import * as auth from './config/auth.js'; import { type LoadedSettings } from './config/settings.js'; @@ -74,6 +75,10 @@ describe('validateNonInterActiveAuth', () => { it('exits if no auth type is configured or env vars set', async () => { const nonInteractiveConfig = { refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), }; try { await validateNonInteractiveAuth( @@ -223,6 +228,10 @@ describe('validateNonInterActiveAuth', () => { vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!'); const nonInteractiveConfig = { refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), }; try { await validateNonInteractiveAuth( @@ -286,6 +295,10 @@ describe('validateNonInterActiveAuth', () => { process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true'; const nonInteractiveConfig = { refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), }; try { await validateNonInteractiveAuth( @@ -303,4 +316,107 @@ describe('validateNonInterActiveAuth', () => { ); expect(processExitSpy).toHaveBeenCalledWith(1); }); + + describe('JSON output mode', () => { + it('prints JSON error when no auth is configured and exits with code 1', async () => { + const nonInteractiveConfig = { + refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), + }; + + let thrown: Error | undefined; + try { + await validateNonInteractiveAuth( + undefined, + undefined, + nonInteractiveConfig as unknown as Config, + mockSettings, + ); + } catch (e) { + thrown = e as Error; + } + + expect(thrown?.message).toBe('process.exit(1) called'); + const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; + const payload = JSON.parse(errorArg); + expect(payload.error.type).toBe('Error'); + expect(payload.error.code).toBe(1); + expect(payload.error.message).toContain( + 'Please set an Auth method in your', + ); + }); + + it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => { + mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI; + process.env['GOOGLE_GENAI_USE_GCA'] = 'true'; + + const nonInteractiveConfig = { + refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), + }; + + let thrown: Error | undefined; + try { + await validateNonInteractiveAuth( + undefined, + undefined, + nonInteractiveConfig as unknown as Config, + mockSettings, + ); + } catch (e) { + thrown = e as Error; + } + + expect(thrown?.message).toBe('process.exit(1) called'); + { + const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; + const payload = JSON.parse(errorArg); + expect(payload.error.type).toBe('Error'); + expect(payload.error.code).toBe(1); + expect(payload.error.message).toContain( + 'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.', + ); + } + }); + + it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => { + vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!'); + process.env['GEMINI_API_KEY'] = 'fake-key'; + + const nonInteractiveConfig = { + refreshAuth: refreshAuthMock, + getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ authType: undefined }), + }; + + let thrown: Error | undefined; + try { + await validateNonInteractiveAuth( + AuthType.USE_GEMINI, + undefined, + nonInteractiveConfig as unknown as Config, + mockSettings, + ); + } catch (e) { + thrown = e as Error; + } + + expect(thrown?.message).toBe('process.exit(1) called'); + { + const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string; + const payload = JSON.parse(errorArg); + expect(payload.error.type).toBe('Error'); + expect(payload.error.code).toBe(1); + expect(payload.error.message).toBe('Auth error!'); + } + }); + }); }); diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts index 1339c1eb4c..9c46766ac9 100644 --- a/packages/cli/src/validateNonInterActiveAuth.ts +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -5,10 +5,11 @@ */ import type { Config } from '@google/gemini-cli-core'; -import { AuthType } from '@google/gemini-cli-core'; +import { AuthType, OutputFormat } from '@google/gemini-cli-core'; import { USER_SETTINGS_PATH } from './config/settings.js'; import { validateAuthMethod } from './config/auth.js'; import { type LoadedSettings } from './config/settings.js'; +import { handleError } from './utils/errors.js'; function getAuthTypeFromEnv(): AuthType | undefined { if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') { @@ -29,35 +30,45 @@ export async function validateNonInteractiveAuth( nonInteractiveConfig: Config, settings: LoadedSettings, ) { - const enforcedType = settings.merged.security?.auth?.enforcedType; - if (enforcedType) { - const currentAuthType = getAuthTypeFromEnv(); - if (currentAuthType !== enforcedType) { - console.error( - `The configured auth type is ${enforcedType}, but the current auth type is ${currentAuthType}. Please re-authenticate with the correct type.`, + try { + const enforcedType = settings.merged.security?.auth?.enforcedType; + if (enforcedType) { + const currentAuthType = getAuthTypeFromEnv(); + if (currentAuthType !== enforcedType) { + const message = `The configured auth type is ${enforcedType}, but the current auth type is ${currentAuthType}. Please re-authenticate with the correct type.`; + throw new Error(message); + } + } + + const effectiveAuthType = + enforcedType || getAuthTypeFromEnv() || configuredAuthType; + + if (!effectiveAuthType) { + const message = `Please set an Auth method in your ${USER_SETTINGS_PATH} or specify one of the following environment variables before running: GEMINI_API_KEY, GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_GENAI_USE_GCA`; + throw new Error(message); + } + + const authType: AuthType = effectiveAuthType as AuthType; + + if (!useExternalAuth) { + const err = validateAuthMethod(String(authType)); + if (err != null) { + throw new Error(err); + } + } + + await nonInteractiveConfig.refreshAuth(authType); + return nonInteractiveConfig; + } catch (error) { + if (nonInteractiveConfig.getOutputFormat() === OutputFormat.JSON) { + handleError( + error instanceof Error ? error : new Error(String(error)), + nonInteractiveConfig, + 1, ); + } else { + console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } - - const effectiveAuthType = - enforcedType || getAuthTypeFromEnv() || configuredAuthType; - - if (!effectiveAuthType) { - console.error( - `Please set an Auth method in your ${USER_SETTINGS_PATH} or specify one of the following environment variables before running: GEMINI_API_KEY, GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_GENAI_USE_GCA`, - ); - process.exit(1); - } - - if (!useExternalAuth) { - const err = validateAuthMethod(effectiveAuthType); - if (err != null) { - console.error(err); - process.exit(1); - } - } - - await nonInteractiveConfig.refreshAuth(effectiveAuthType); - return nonInteractiveConfig; }