/** * @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(); 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'], }), ); }); }); });