Update error codes when process exiting the gemini cli (#13728)

This commit is contained in:
Megha Bansal
2025-11-26 08:13:21 +05:30
committed by GitHub
parent d2a6cff4df
commit d97bbd5324
12 changed files with 307 additions and 192 deletions

View File

@@ -7,6 +7,7 @@
import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import { ExitCodes } from '@google/gemini-cli-core/src/index.js';
describe('JSON output', () => {
let rig: TestRig;
@@ -81,7 +82,7 @@ describe('JSON output', () => {
expect(payload.error).toBeDefined();
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toContain(
"enforced authentication type is 'gemini-api-key'",
);

View File

@@ -27,7 +27,7 @@ describe('mixed input crash prevention', () => {
expect(error).toBeInstanceOf(Error);
const err = error as Error;
expect(err.message).toContain('Process exited with code 1');
expect(err.message).toContain('Process exited with code 42');
expect(err.message).toContain(
'--prompt-interactive flag cannot be used when input is piped',
);

View File

@@ -22,6 +22,7 @@ import {
} from './gemini.js';
import os from 'node:os';
import v8 from 'node:v8';
import { type CliArgs } from './config/config.js';
import { type LoadedSettings } from './config/settings.js';
import { appEvents, AppEvent } from './utils/events.js';
import {
@@ -210,13 +211,11 @@ describe('gemini.tsx main function', () => {
}
const currentListeners = process.listeners('unhandledRejection');
const addedListener = currentListeners.find(
(listener) => !initialUnhandledRejectionListeners.includes(listener),
);
if (addedListener) {
process.removeListener('unhandledRejection', addedListener);
}
currentListeners.forEach((listener) => {
if (!initialUnhandledRejectionListeners.includes(listener)) {
process.removeListener('unhandledRejection', listener);
}
});
vi.restoreAllMocks();
});
@@ -698,64 +697,6 @@ describe('gemini.tsx main function kitty protocol', () => {
processExitSpy.mockRestore();
});
it('should exit with error when --prompt-interactive is used with piped input', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
const core = await import('@google/gemini-cli-core');
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const writeToStderrSpy = vi
.spyOn(core, 'writeToStderr')
.mockImplementation(() => true);
vi.mocked(loadSettings).mockReturnValue({
merged: { advanced: {}, security: { auth: {} }, ui: {} },
setValue: vi.fn(),
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
errors: [],
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
vi.mocked(parseArguments).mockResolvedValue({
promptInteractive: true,
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => false,
getQuestion: () => '',
getSandbox: () => false,
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
// Mock stdin to be non-TTY
Object.defineProperty(process.stdin, 'isTTY', {
value: false,
configurable: true,
});
try {
await main();
} catch (e) {
if (!(e instanceof MockProcessExitError)) throw e;
}
expect(writeToStderrSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Error: The --prompt-interactive flag cannot be used',
),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
writeToStderrSpy.mockRestore();
Object.defineProperty(process.stdin, 'isTTY', {
value: true,
configurable: true,
}); // Restore TTY
});
it('should log warning when theme is not found', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
@@ -836,13 +777,15 @@ describe('gemini.tsx main function kitty protocol', () => {
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
vi.mock('./utils/sessionUtils.js', () => ({
SessionSelector: class {
resolveSession = vi
.fn()
.mockRejectedValue(new Error('Session not found'));
},
}));
const { SessionSelector } = await import('./utils/sessionUtils.js');
vi.mocked(SessionSelector).mockImplementation(
() =>
({
resolveSession: vi
.fn()
.mockRejectedValue(new Error('Session not found')),
}) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
);
const processExitSpy = vi
.spyOn(process, 'exit')
@@ -905,7 +848,7 @@ describe('gemini.tsx main function kitty protocol', () => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error resuming session: Session not found'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(42);
processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
@@ -989,83 +932,6 @@ describe('gemini.tsx main function kitty protocol', () => {
processExitSpy.mockRestore();
});
it('should handle refreshAuth failure', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
vi.mocked(loadSettings).mockReturnValue({
merged: {
advanced: {},
security: { auth: { selectedType: 'google' } },
ui: {},
},
setValue: vi.fn(),
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
errors: [],
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
vi.mocked(loadSandboxConfig).mockResolvedValue({} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
vi.mocked(parseArguments).mockResolvedValue({
promptInteractive: false,
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => true,
getQuestion: () => '',
getSandbox: () => false,
getDebugMode: () => false,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
initialize: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getProjectRoot: () => '/',
getListExtensions: () => false,
getListSessions: () => false,
getDeleteSession: () => undefined,
getToolRegistry: vi.fn(),
getExtensions: () => [],
getModel: () => 'gemini-pro',
getEmbeddingModel: () => 'embedding-001',
getApprovalMode: () => 'default',
getCoreTools: () => [],
getTelemetryEnabled: () => false,
getTelemetryLogPromptsEnabled: () => false,
getFileFilteringRespectGitIgnore: () => true,
getOutputFormat: () => 'text',
getUsageStatisticsEnabled: () => false,
refreshAuth: vi.fn().mockRejectedValue(new Error('Auth refresh failed')),
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
try {
await main();
} catch (e) {
if (!(e instanceof MockProcessExitError)) throw e;
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Error authenticating:',
expect.any(Error),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
});
it('should read from stdin in non-interactive mode', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
@@ -1160,6 +1026,204 @@ describe('gemini.tsx main function kitty protocol', () => {
});
});
describe('gemini.tsx main function exit codes', () => {
let originalEnvNoRelaunch: string | undefined;
beforeEach(() => {
originalEnvNoRelaunch = process.env['GEMINI_CLI_NO_RELAUNCH'];
process.env['GEMINI_CLI_NO_RELAUNCH'] = 'true';
vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new MockProcessExitError(code);
});
// Mock stderr to avoid cluttering output
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
if (originalEnvNoRelaunch !== undefined) {
process.env['GEMINI_CLI_NO_RELAUNCH'] = originalEnvNoRelaunch;
} else {
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
}
vi.restoreAllMocks();
});
it('should exit with 42 for invalid input combination (prompt-interactive with non-TTY)', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
vi.mocked(loadCliConfig).mockResolvedValue({} as Config);
vi.mocked(loadSettings).mockReturnValue({
merged: { security: { auth: {} }, ui: {} },
errors: [],
} as never);
vi.mocked(parseArguments).mockResolvedValue({
promptInteractive: true,
} as unknown as CliArgs);
Object.defineProperty(process.stdin, 'isTTY', {
value: false,
configurable: true,
});
try {
await main();
expect.fail('Should have thrown MockProcessExitError');
} catch (e) {
expect(e).toBeInstanceOf(MockProcessExitError);
expect((e as MockProcessExitError).code).toBe(42);
}
});
it('should exit with 41 for auth failure during sandbox setup', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(loadSandboxConfig).mockResolvedValue({} as any);
vi.mocked(loadCliConfig).mockResolvedValue({
refreshAuth: vi.fn().mockRejectedValue(new Error('Auth failed')),
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
merged: {
security: { auth: { selectedType: 'google', useExternal: false } },
ui: {},
},
errors: [],
} as never);
vi.mocked(parseArguments).mockResolvedValue({} as unknown as CliArgs);
vi.mock('./config/auth.js', () => ({
validateAuthMethod: vi.fn().mockReturnValue(null),
}));
try {
await main();
expect.fail('Should have thrown MockProcessExitError');
} catch (e) {
expect(e).toBeInstanceOf(MockProcessExitError);
expect((e as MockProcessExitError).code).toBe(41);
}
});
it('should exit with 42 for session resume failure', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => false,
getQuestion: () => 'test',
getSandbox: () => false,
getDebugMode: () => false,
getListExtensions: () => false,
getListSessions: () => false,
getDeleteSession: () => undefined,
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
initialize: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
getEmbeddingModel: () => 'embedding-001',
getApprovalMode: () => 'default',
getCoreTools: () => [],
getTelemetryEnabled: () => false,
getTelemetryLogPromptsEnabled: () => false,
getFileFilteringRespectGitIgnore: () => true,
getOutputFormat: () => 'text',
getExtensions: () => [],
getUsageStatisticsEnabled: () => false,
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
merged: { security: { auth: {} }, ui: {} },
errors: [],
} as never);
vi.mocked(parseArguments).mockResolvedValue({
resume: 'invalid-session',
} as unknown as CliArgs);
vi.mock('./utils/sessionUtils.js', () => ({
SessionSelector: vi.fn().mockImplementation(() => ({
resolveSession: vi
.fn()
.mockRejectedValue(new Error('Session not found')),
})),
}));
try {
await main();
expect.fail('Should have thrown MockProcessExitError');
} catch (e) {
expect(e).toBeInstanceOf(MockProcessExitError);
expect((e as MockProcessExitError).code).toBe(42);
}
});
it('should exit with 42 for no input provided', async () => {
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => false,
getQuestion: () => '',
getSandbox: () => false,
getDebugMode: () => false,
getListExtensions: () => false,
getListSessions: () => false,
getDeleteSession: () => undefined,
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
initialize: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
getToolRegistry: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getModel: () => 'gemini-pro',
getEmbeddingModel: () => 'embedding-001',
getApprovalMode: () => 'default',
getCoreTools: () => [],
getTelemetryEnabled: () => false,
getTelemetryLogPromptsEnabled: () => false,
getFileFilteringRespectGitIgnore: () => true,
getOutputFormat: () => 'text',
getExtensions: () => [],
getUsageStatisticsEnabled: () => false,
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
merged: { security: { auth: {} }, ui: {} },
errors: [],
} as never);
vi.mocked(parseArguments).mockResolvedValue({} as unknown as CliArgs);
Object.defineProperty(process.stdin, 'isTTY', {
value: true, // Simulate TTY so it doesn't try to read stdin
configurable: true,
});
try {
await main();
expect.fail('Should have thrown MockProcessExitError');
} catch (e) {
expect(e).toBeInstanceOf(MockProcessExitError);
expect((e as MockProcessExitError).code).toBe(42);
}
});
});
describe('validateDnsResolutionOrder', () => {
let debugLoggerWarnSpy: ReturnType<typeof vi.spyOn>;
@@ -1278,7 +1342,6 @@ describe('startInteractiveUI', () => {
);
// Verify render was called with correct options
expect(renderSpy).toHaveBeenCalledTimes(1);
const [reactElement, options] = renderSpy.mock.calls[0];
// Verify render options

View File

@@ -56,6 +56,7 @@ import {
enterAlternateScreen,
disableLineWrapping,
shouldEnterAlternateScreen,
ExitCodes,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -310,7 +311,7 @@ export async function main() {
'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.\n',
);
await runExitCleanup();
process.exit(1);
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
const isDebugMode = cliConfig.isDebugMode(argv);
@@ -396,7 +397,7 @@ export async function main() {
} catch (err) {
debugLogger.error('Error authenticating:', err);
await runExitCleanup();
process.exit(1);
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
}
}
let stdinData = '';
@@ -433,7 +434,7 @@ export async function main() {
start_sandbox(sandboxConfig, memoryArgs, partialConfig, sandboxArgs),
);
await runExitCleanup();
process.exit(0);
process.exit(ExitCodes.SUCCESS);
} else {
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
@@ -464,14 +465,14 @@ export async function main() {
debugLogger.log(`- ${extension.name}`);
}
await runExitCleanup();
process.exit(0);
process.exit(ExitCodes.SUCCESS);
}
// Handle --list-sessions flag
if (config.getListSessions()) {
await listSessions(config);
await runExitCleanup();
process.exit(0);
process.exit(ExitCodes.SUCCESS);
}
// Handle --delete-session flag
@@ -479,7 +480,7 @@ export async function main() {
if (sessionToDelete) {
await deleteSession(config, sessionToDelete);
await runExitCleanup();
process.exit(0);
process.exit(ExitCodes.SUCCESS);
}
const wasRaw = process.stdin.isRaw;
@@ -551,7 +552,7 @@ export async function main() {
`Error resuming session: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
await runExitCleanup();
process.exit(1);
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
}
@@ -583,7 +584,7 @@ export async function main() {
`No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`,
);
await runExitCleanup();
process.exit(1);
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
const prompt_id = Math.random().toString(16).slice(2);
@@ -623,7 +624,7 @@ export async function main() {
});
// Call cleanup before process.exit, which causes cleanup to not run
await runExitCleanup();
process.exit(0);
process.exit(ExitCodes.SUCCESS);
}
}

View File

@@ -9,6 +9,7 @@ import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
import { vi } from 'vitest';
import { FolderTrustDialog } from './FolderTrustDialog.js';
import { ExitCodes } from '@google/gemini-cli-core';
import * as processUtils from '../../utils/processUtils.js';
vi.mock('../../utils/processUtils.js', () => ({
@@ -61,7 +62,9 @@ describe('FolderTrustDialog', () => {
);
});
await waitFor(() => {
expect(mockedExit).toHaveBeenCalledWith(1);
expect(mockedExit).toHaveBeenCalledWith(
ExitCodes.FATAL_CANCELLATION_ERROR,
);
});
expect(onSelect).not.toHaveBeenCalled();
});

View File

@@ -14,6 +14,8 @@ import { useKeypress } from '../hooks/useKeypress.js';
import * as process from 'node:process';
import * as path from 'node:path';
import { relaunchApp } from '../../utils/processUtils.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { ExitCodes } from '@google/gemini-cli-core';
export enum FolderTrustChoice {
TRUST_FOLDER = 'trust_folder',
@@ -46,8 +48,9 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
(key) => {
if (key.name === 'escape') {
setExiting(true);
setTimeout(() => {
process.exit(1);
setTimeout(async () => {
await runExitCleanup();
process.exit(ExitCodes.FATAL_CANCELLATION_ERROR);
}, 100);
}
},

View File

@@ -14,7 +14,7 @@ import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
import { TrustLevel } from '../../config/trustedFolders.js';
import * as trustedFolders from '../../config/trustedFolders.js';
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents, ExitCodes } from '@google/gemini-cli-core';
const mockedCwd = vi.hoisted(() => vi.fn());
const mockedExit = vi.hoisted(() => vi.fn());
@@ -266,7 +266,7 @@ describe('useFolderTrust', () => {
expect(result.current.isFolderTrustDialogOpen).toBe(false); // Dialog should close
});
it('should emit feedback on failure to set value', () => {
it('should emit feedback on failure to set value', async () => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: undefined,
source: undefined,
@@ -283,12 +283,12 @@ describe('useFolderTrust', () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
});
vi.runAllTimers();
await vi.runAllTimersAsync();
expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Failed to save trust settings. Exiting Gemini CLI.',
);
expect(mockedExit).toHaveBeenCalledWith(1);
expect(mockedExit).toHaveBeenCalledWith(ExitCodes.FATAL_CONFIG_ERROR);
});
});

View File

@@ -14,7 +14,8 @@ import {
} from '../../config/trustedFolders.js';
import * as process from 'node:process';
import { type HistoryItemWithoutId, MessageType } from '../types.js';
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents, ExitCodes } from '@google/gemini-cli-core';
import { runExitCleanup } from '../../utils/cleanup.js';
export const useFolderTrust = (
settings: LoadedSettings,
@@ -75,8 +76,9 @@ export const useFolderTrust = (
'error',
'Failed to save trust settings. Exiting Gemini CLI.',
);
setTimeout(() => {
process.exit(1);
setTimeout(async () => {
await runExitCleanup();
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
}, 100);
return;
}

View File

@@ -20,6 +20,7 @@ import {
OutputFormat,
makeFakeConfig,
debugLogger,
ExitCodes,
} from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import * as auth from './config/auth.js';
@@ -116,12 +117,16 @@ describe('validateNonInterActiveAuth', () => {
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Please set an Auth method'),
);
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set', async () => {
@@ -268,10 +273,14 @@ describe('validateNonInterActiveAuth', () => {
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith('Auth error!');
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('skips validation if useExternalAuth is true', async () => {
@@ -329,12 +338,16 @@ describe('validateNonInterActiveAuth', () => {
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
"The enforced authentication type is 'oauth-personal', but the current type is 'gemini-api-key'. Please re-authenticate with the correct type.",
);
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
it('exits if auth from env var does not match enforcedAuthType', async () => {
@@ -354,16 +367,20 @@ describe('validateNonInterActiveAuth', () => {
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
expect((e as Error).message).toContain(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) called`,
);
}
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
"The enforced authentication type is 'oauth-personal', but the current type is 'gemini-api-key'. Please re-authenticate with the correct type.",
);
expect(processExitSpy).toHaveBeenCalledWith(1);
expect(processExitSpy).toHaveBeenCalledWith(
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
});
describe('JSON output mode', () => {
it('prints JSON error when no auth is configured and exits with code 1', async () => {
it(`prints JSON error when no auth is configured and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
const nonInteractiveConfig = createLocalMockConfig({
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
@@ -384,17 +401,19 @@ describe('validateNonInterActiveAuth', () => {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) 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.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
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 () => {
it(`prints JSON error when enforced auth mismatches current auth and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
const nonInteractiveConfig = createLocalMockConfig({
refreshAuth: refreshAuthMock,
@@ -416,19 +435,21 @@ describe('validateNonInterActiveAuth', () => {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) 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.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toContain(
"The enforced authentication type is 'gemini-api-key', but the current type is 'oauth-personal'. Please re-authenticate with the correct type.",
);
}
});
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
it(`prints JSON error when validateAuthMethod fails and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['GEMINI_API_KEY'] = 'fake-key';
@@ -452,12 +473,14 @@ describe('validateNonInterActiveAuth', () => {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
expect(thrown?.message).toBe(
`process.exit(${ExitCodes.FATAL_AUTHENTICATION_ERROR}) 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.code).toBe(ExitCodes.FATAL_AUTHENTICATION_ERROR);
expect(payload.error.message).toBe('Auth error!');
}
});

View File

@@ -5,7 +5,12 @@
*/
import type { Config } from '@google/gemini-cli-core';
import { AuthType, debugLogger, OutputFormat } from '@google/gemini-cli-core';
import {
AuthType,
debugLogger,
OutputFormat,
ExitCodes,
} 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';
@@ -63,12 +68,12 @@ export async function validateNonInteractiveAuth(
handleError(
error instanceof Error ? error : new Error(String(error)),
nonInteractiveConfig,
1,
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);
} else {
debugLogger.error(error instanceof Error ? error.message : String(error));
await runExitCleanup();
process.exit(1);
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
}
}
}

View File

@@ -47,6 +47,7 @@ export * from './core/apiKeyCredentialStorage.js';
export * from './utils/paths.js';
export * from './utils/schemaValidator.js';
export * from './utils/errors.js';
export * from './utils/exitCodes.js';
export * from './utils/getFolderStructure.js';
export * from './utils/memoryDiscovery.js';
export * from './utils/getPty.js';

View File

@@ -0,0 +1,13 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export const ExitCodes = {
SUCCESS: 0,
FATAL_AUTHENTICATION_ERROR: 41,
FATAL_INPUT_ERROR: 42,
FATAL_CONFIG_ERROR: 52,
FATAL_CANCELLATION_ERROR: 130,
} as const;