mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-15 05:47:18 -07:00
314 lines
10 KiB
TypeScript
314 lines
10 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as path from 'node:path';
|
|
import { loadConfig } from './config.js';
|
|
import type { Settings } from './settings.js';
|
|
import {
|
|
type ExtensionLoader,
|
|
FileDiscoveryService,
|
|
getCodeAssistServer,
|
|
Config,
|
|
ExperimentFlags,
|
|
fetchAdminControlsOnce,
|
|
type FetchAdminControlsResponse,
|
|
} from '@google/gemini-cli-core';
|
|
|
|
// Mock dependencies
|
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
|
return {
|
|
...actual,
|
|
Config: vi.fn().mockImplementation((params) => {
|
|
const mockConfig = {
|
|
...params,
|
|
initialize: vi.fn(),
|
|
refreshAuth: vi.fn(),
|
|
getExperiments: vi.fn().mockReturnValue({
|
|
flags: {
|
|
[actual.ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
|
|
boolValue: false,
|
|
},
|
|
},
|
|
}),
|
|
getRemoteAdminSettings: vi.fn(),
|
|
setRemoteAdminSettings: vi.fn(),
|
|
};
|
|
return mockConfig;
|
|
}),
|
|
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
|
|
memoryContent: { global: '', extension: '', project: '' },
|
|
fileCount: 0,
|
|
filePaths: [],
|
|
}),
|
|
startupProfiler: {
|
|
flush: vi.fn(),
|
|
},
|
|
FileDiscoveryService: vi.fn(),
|
|
getCodeAssistServer: vi.fn(),
|
|
fetchAdminControlsOnce: vi.fn(),
|
|
coreEvents: {
|
|
emitAdminSettingsChanged: vi.fn(),
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock('../utils/logger.js', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('loadConfig', () => {
|
|
const mockSettings = {} as Settings;
|
|
const mockExtensionLoader = {} as ExtensionLoader;
|
|
const taskId = 'test-task-id';
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
process.env['GEMINI_API_KEY'] = 'test-key';
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env['CUSTOM_IGNORE_FILE_PATHS'];
|
|
delete process.env['GEMINI_API_KEY'];
|
|
});
|
|
|
|
describe('admin settings overrides', () => {
|
|
it('should not fetch admin controls if experiment is disabled', async () => {
|
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
expect(fetchAdminControlsOnce).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe('when admin controls experiment is enabled', () => {
|
|
beforeEach(() => {
|
|
// We need to cast to any here to modify the mock implementation
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(Config as any).mockImplementation((params: unknown) => {
|
|
const mockConfig = {
|
|
...(params as object),
|
|
initialize: vi.fn(),
|
|
refreshAuth: vi.fn(),
|
|
getExperiments: vi.fn().mockReturnValue({
|
|
flags: {
|
|
[ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
|
|
boolValue: true,
|
|
},
|
|
},
|
|
}),
|
|
getRemoteAdminSettings: vi.fn().mockReturnValue({}),
|
|
setRemoteAdminSettings: vi.fn(),
|
|
};
|
|
return mockConfig;
|
|
});
|
|
});
|
|
|
|
it('should fetch admin controls and apply them', async () => {
|
|
const mockAdminSettings: FetchAdminControlsResponse = {
|
|
mcpSetting: {
|
|
mcpEnabled: false,
|
|
},
|
|
cliFeatureSetting: {
|
|
extensionsSetting: {
|
|
extensionsEnabled: false,
|
|
},
|
|
},
|
|
strictModeDisabled: false,
|
|
};
|
|
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
|
|
|
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
|
|
expect(Config).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
|
extensionsEnabled:
|
|
mockAdminSettings.cliFeatureSetting?.extensionsSetting
|
|
?.extensionsEnabled,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should treat unset admin settings as false when admin settings are passed', async () => {
|
|
const mockAdminSettings: FetchAdminControlsResponse = {
|
|
mcpSetting: {
|
|
mcpEnabled: true,
|
|
},
|
|
};
|
|
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
|
|
|
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
|
|
expect(Config).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
disableYoloMode: !false,
|
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
|
extensionsEnabled: undefined,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not pass default unset admin settings when no admin settings are present', async () => {
|
|
const mockAdminSettings: FetchAdminControlsResponse = {};
|
|
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
|
|
|
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
|
|
expect(Config).toHaveBeenLastCalledWith(expect.objectContaining({}));
|
|
});
|
|
|
|
it('should fetch admin controls using the code assist server when available', async () => {
|
|
const mockAdminSettings: FetchAdminControlsResponse = {
|
|
mcpSetting: {
|
|
mcpEnabled: true,
|
|
},
|
|
strictModeDisabled: true,
|
|
};
|
|
const mockCodeAssistServer = { projectId: 'test-project' };
|
|
vi.mocked(getCodeAssistServer).mockReturnValue(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
mockCodeAssistServer as any,
|
|
);
|
|
vi.mocked(fetchAdminControlsOnce).mockResolvedValue(mockAdminSettings);
|
|
|
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
|
|
expect(fetchAdminControlsOnce).toHaveBeenCalledWith(
|
|
mockCodeAssistServer,
|
|
true,
|
|
);
|
|
expect(Config).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
|
extensionsEnabled: undefined,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => {
|
|
const testPath = '/tmp/ignore';
|
|
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
|
|
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
|
|
testPath,
|
|
]);
|
|
});
|
|
|
|
it('should set customIgnoreFilePaths when settings.fileFiltering.customIgnoreFilePaths is present', async () => {
|
|
const testPath = '/settings/ignore';
|
|
const settings: Settings = {
|
|
fileFiltering: {
|
|
customIgnoreFilePaths: [testPath],
|
|
},
|
|
};
|
|
const config = await loadConfig(settings, mockExtensionLoader, taskId);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
|
|
testPath,
|
|
]);
|
|
});
|
|
|
|
it('should merge customIgnoreFilePaths from settings and env var', async () => {
|
|
const envPath = '/env/ignore';
|
|
const settingsPath = '/settings/ignore';
|
|
process.env['CUSTOM_IGNORE_FILE_PATHS'] = envPath;
|
|
const settings: Settings = {
|
|
fileFiltering: {
|
|
customIgnoreFilePaths: [settingsPath],
|
|
},
|
|
};
|
|
const config = await loadConfig(settings, mockExtensionLoader, taskId);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
|
|
settingsPath,
|
|
envPath,
|
|
]);
|
|
});
|
|
|
|
it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => {
|
|
const paths = ['/path/one', '/path/two'];
|
|
process.env['CUSTOM_IGNORE_FILE_PATHS'] = paths.join(path.delimiter);
|
|
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual(paths);
|
|
});
|
|
|
|
it('should have empty customIgnoreFilePaths when both are missing', async () => {
|
|
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]);
|
|
});
|
|
|
|
it('should initialize FileDiscoveryService with correct options', async () => {
|
|
const testPath = '/tmp/ignore';
|
|
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
|
|
const settings: Settings = {
|
|
fileFiltering: {
|
|
respectGitIgnore: false,
|
|
},
|
|
};
|
|
|
|
await loadConfig(settings, mockExtensionLoader, taskId);
|
|
|
|
expect(FileDiscoveryService).toHaveBeenCalledWith(expect.any(String), {
|
|
respectGitIgnore: false,
|
|
respectGeminiIgnore: undefined,
|
|
customIgnoreFilePaths: [testPath],
|
|
});
|
|
});
|
|
|
|
describe('tool configuration', () => {
|
|
it('should pass V1 allowedTools to Config properly', async () => {
|
|
const settings: Settings = {
|
|
allowedTools: ['shell', 'edit'],
|
|
};
|
|
await loadConfig(settings, mockExtensionLoader, taskId);
|
|
expect(Config).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
allowedTools: ['shell', 'edit'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should pass V2 tools.allowed to Config properly', async () => {
|
|
const settings: Settings = {
|
|
tools: {
|
|
allowed: ['shell', 'fetch'],
|
|
},
|
|
};
|
|
await loadConfig(settings, mockExtensionLoader, taskId);
|
|
expect(Config).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
allowedTools: ['shell', 'fetch'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should prefer V1 allowedTools over V2 tools.allowed if both present', async () => {
|
|
const settings: Settings = {
|
|
allowedTools: ['v1-tool'],
|
|
tools: {
|
|
allowed: ['v2-tool'],
|
|
},
|
|
};
|
|
await loadConfig(settings, mockExtensionLoader, taskId);
|
|
expect(Config).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
allowedTools: ['v1-tool'],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|