mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Update error codes when process exiting the gemini cli (#13728)
This commit is contained in:
@@ -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'",
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
13
packages/core/src/utils/exitCodes.ts
Normal file
13
packages/core/src/utils/exitCodes.ts
Normal 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;
|
||||
Reference in New Issue
Block a user