Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -4,18 +4,24 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import {
main,
setupUnhandledRejectionHandler,
validateDnsResolutionOrder,
startInteractiveUI,
} from './gemini.js';
import type { SettingsFile } from './config/settings.js';
import { LoadedSettings, loadSettings } from './config/settings.js';
import { type LoadedSettings } from './config/settings.js';
import { appEvents, AppEvent } from './utils/events.js';
import type { Config } from '@qwen-code/qwen-code-core';
import { FatalConfigError } from '@qwen-code/qwen-code-core';
// Custom error to identify mock process.exit calls
class MockProcessExitError extends Error {
@@ -36,14 +42,12 @@ vi.mock('./config/settings.js', async (importOriginal) => {
vi.mock('./config/config.js', () => ({
loadCliConfig: vi.fn().mockResolvedValue({
config: {
getSandbox: vi.fn(() => false),
getQuestion: vi.fn(() => ''),
},
modelWasSwitched: false,
originalModelBeforeSwitch: null,
finalModel: 'test-model',
}),
getSandbox: vi.fn(() => false),
getQuestion: vi.fn(() => ''),
isInteractive: () => false,
} as unknown as Config),
parseArguments: vi.fn().mockResolvedValue({}),
isDebugMode: vi.fn(() => false),
}));
vi.mock('read-package-up', () => ({
@@ -74,22 +78,21 @@ vi.mock('./utils/sandbox.js', () => ({
start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
}));
vi.mock('./utils/relaunch.js', () => ({
relaunchAppInChildProcess: vi.fn(),
}));
vi.mock('./config/sandboxConfig.js', () => ({
loadSandboxConfig: vi.fn(),
}));
describe('gemini.tsx main function', () => {
let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>;
let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined;
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
[];
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
beforeEach(() => {
loadSettingsMock = vi.mocked(loadSettings);
// Store and clear sandbox-related env variables to ensure a consistent test environment
originalEnvGeminiSandbox = process.env['GEMINI_SANDBOX'];
originalEnvSandbox = process.env['SANDBOX'];
@@ -124,43 +127,73 @@ describe('gemini.tsx main function', () => {
vi.restoreAllMocks();
});
it('should throw InvalidConfigurationError if settings have errors', async () => {
const settingsError = {
message: 'Test settings error',
path: '/test/settings.json',
};
const userSettingsFile: SettingsFile = {
path: '/user/settings.json',
settings: {},
};
const workspaceSettingsFile: SettingsFile = {
path: '/workspace/.gemini/settings.json',
settings: {},
};
const systemSettingsFile: SettingsFile = {
path: '/system/settings.json',
settings: {},
};
const systemDefaultsFile: SettingsFile = {
path: '/system/system-defaults.json',
settings: {},
};
const mockLoadedSettings = new LoadedSettings(
systemSettingsFile,
systemDefaultsFile,
userSettingsFile,
workspaceSettingsFile,
[settingsError],
true,
new Set(),
);
it('verifies that we dont load the config before relaunchAppInChildProcess', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const { relaunchAppInChildProcess } = await import('./utils/relaunch.js');
const { loadCliConfig } = await import('./config/config.js');
const { loadSettings } = await import('./config/settings.js');
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
loadSettingsMock.mockReturnValue(mockLoadedSettings);
const callOrder: string[] = [];
vi.mocked(relaunchAppInChildProcess).mockImplementation(async () => {
callOrder.push('relaunch');
});
vi.mocked(loadCliConfig).mockImplementation(async () => {
callOrder.push('loadCliConfig');
return {
isInteractive: () => false,
getQuestion: () => '',
getSandbox: () => false,
getDebugMode: () => false,
getListExtensions: () => false,
getMcpServers: () => ({}),
initialize: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getProjectRoot: () => '/',
} as unknown as Config;
});
vi.mocked(loadSettings).mockReturnValue({
errors: [],
merged: {
advanced: { autoConfigureMemory: true },
security: { auth: {} },
ui: {},
},
setValue: vi.fn(),
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
} as never);
try {
await main();
} catch (e) {
// Mocked process exit throws an error.
if (!(e instanceof MockProcessExitError)) throw e;
}
await expect(main()).rejects.toThrow(FatalConfigError);
// It is critical that we call relaunch before loadCliConfig to avoid
// loading config in the outer process when we are going to relaunch.
// By ensuring we don't load the config we also ensure we don't trigger any
// operations that might require loading the config such as such as
// initializing mcp servers.
// For the sandbox case we still have to load a partial cli config.
// we can authorize outside the sandbox.
expect(callOrder).toEqual(['relaunch', 'loadCliConfig']);
processExitSpy.mockRestore();
});
it('should log unhandled promise rejections and open debug console on first error', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const appEventsMock = vi.mocked(appEvents);
const rejectionError = new Error('Test unhandled rejection');
@@ -199,6 +232,117 @@ describe('gemini.tsx main function', () => {
});
});
describe('gemini.tsx main function kitty protocol', () => {
let originalEnvNoRelaunch: string | undefined;
let setRawModeSpy: MockInstance<
(mode: boolean) => NodeJS.ReadStream & { fd: 0 }
>;
beforeEach(() => {
// Set no relaunch in tests since process spawning causing issues in tests
originalEnvNoRelaunch = process.env['GEMINI_CLI_NO_RELAUNCH'];
process.env['GEMINI_CLI_NO_RELAUNCH'] = 'true';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(process.stdin as any).setRawMode) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process.stdin as any).setRawMode = vi.fn();
}
setRawModeSpy = vi.spyOn(process.stdin, 'setRawMode');
Object.defineProperty(process.stdin, 'isTTY', {
value: true,
configurable: true,
});
Object.defineProperty(process.stdin, 'isRaw', {
value: false,
configurable: true,
});
});
afterEach(() => {
// Restore original env variables
if (originalEnvNoRelaunch !== undefined) {
process.env['GEMINI_CLI_NO_RELAUNCH'] = originalEnvNoRelaunch;
} else {
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
}
});
it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => {
const { detectAndEnableKittyProtocol } = await import(
'./ui/utils/kittyProtocolDetector.js'
);
const { loadCliConfig, parseArguments } = await import(
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => true,
getQuestion: () => '',
getSandbox: () => false,
getDebugMode: () => false,
getListExtensions: () => false,
getMcpServers: () => ({}),
initialize: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
errors: [],
merged: {
advanced: {},
security: { auth: {} },
ui: {},
},
setValue: vi.fn(),
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
} as never);
vi.mocked(parseArguments).mockResolvedValue({
model: undefined,
sandbox: undefined,
sandboxImage: undefined,
debug: undefined,
prompt: undefined,
promptInteractive: undefined,
query: undefined,
allFiles: undefined,
showMemoryUsage: undefined,
yolo: undefined,
approvalMode: undefined,
telemetry: undefined,
checkpointing: undefined,
telemetryTarget: undefined,
telemetryOtlpEndpoint: undefined,
telemetryOtlpProtocol: undefined,
telemetryLogPrompts: undefined,
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
experimentalAcp: undefined,
extensions: undefined,
listExtensions: undefined,
openaiLogging: undefined,
openaiApiKey: undefined,
openaiBaseUrl: undefined,
proxy: undefined,
includeDirectories: undefined,
tavilyApiKey: undefined,
screenReader: undefined,
vlmSwitchMode: undefined,
useSmartEdit: undefined,
outputFormat: undefined,
});
await main();
expect(setRawModeSpy).toHaveBeenCalledWith(true);
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
});
});
describe('validateDnsResolutionOrder', () => {
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
@@ -227,7 +371,6 @@ describe('validateDnsResolutionOrder', () => {
it('should return the default "ipv4first" and log a warning for an invalid string', () => {
expect(validateDnsResolutionOrder('invalid-value')).toBe('ipv4first');
expect(consoleWarnSpy).toHaveBeenCalledOnce();
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Invalid value for dnsResolutionOrder in settings: "invalid-value". Using default "ipv4first".',
);
@@ -255,7 +398,7 @@ describe('startInteractiveUI', () => {
}));
vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({
detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve()),
detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve(true)),
}));
vi.mock('./ui/utils/updateCheck.js', () => ({
@@ -279,11 +422,19 @@ describe('startInteractiveUI', () => {
const { render } = await import('ink');
const renderSpy = vi.mocked(render);
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
// Verify render was called with correct options
@@ -302,22 +453,26 @@ describe('startInteractiveUI', () => {
it('should perform all startup tasks in correct order', async () => {
const { getCliVersion } = await import('./utils/version.js');
const { detectAndEnableKittyProtocol } = await import(
'./ui/utils/kittyProtocolDetector.js'
);
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const { registerCleanup } = await import('./utils/cleanup.js');
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
// Verify all startup tasks were called
expect(getCliVersion).toHaveBeenCalledTimes(1);
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
expect(registerCleanup).toHaveBeenCalledTimes(1);
// Verify cleanup handler is registered with unmount function