mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 21:44:25 -07:00
Update error codes when process exiting the gemini cli (#13728)
This commit is contained in:
+214
-151
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user