/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { // Schema utilities getSettingsByCategory, getSettingDefinition, requiresRestart, getDefaultValue, getRestartRequiredSettings, getEffectiveValue, getAllSettingKeys, getSettingsByType, getSettingsRequiringRestart, isValidSettingKey, getSettingCategory, shouldShowInDialog, getDialogSettingsByCategory, getDialogSettingsByType, getDialogSettingKeys, // Business logic utilities, TEST_ONLY, isInSettingsScope, getDisplayValue, } from './settingsUtils.js'; import { getSettingsSchema, type SettingDefinition, type Settings, type SettingsSchema, type SettingsSchemaType, } from '../config/settingsSchema.js'; vi.mock('../config/settingsSchema.js', async (importOriginal) => { const original = await importOriginal(); return { ...original, getSettingsSchema: vi.fn(), }; }); function makeMockSettings(settings: unknown): Settings { return settings as Settings; } describe('SettingsUtils', () => { beforeEach(() => { const SETTINGS_SCHEMA = { mcpServers: { type: 'object', label: 'MCP Servers', category: 'Advanced', requiresRestart: true, default: {} as Record, description: 'Configuration for MCP servers.', showInDialog: false, }, test: { type: 'string', label: 'Test', category: 'Basic', requiresRestart: false, default: 'hello', description: 'A test field', showInDialog: true, }, advanced: { type: 'object', label: 'Advanced', category: 'Advanced', requiresRestart: true, default: {}, description: 'Advanced settings for power users.', showInDialog: false, properties: { autoConfigureMemory: { type: 'boolean', label: 'Auto Configure Max Old Space Size', category: 'Advanced', requiresRestart: true, default: false, description: 'Automatically configure Node.js memory limits', showInDialog: true, }, }, }, ui: { type: 'object', label: 'UI', category: 'UI', requiresRestart: false, default: {}, description: 'User interface settings.', showInDialog: false, properties: { theme: { type: 'string', label: 'Theme', category: 'UI', requiresRestart: false, default: undefined as string | undefined, description: 'The color theme for the UI.', showInDialog: false, }, requiresRestart: { type: 'boolean', label: 'Requires Restart', category: 'UI', default: false, requiresRestart: true, }, accessibility: { type: 'object', label: 'Accessibility', category: 'UI', requiresRestart: true, default: {}, description: 'Accessibility settings.', showInDialog: false, properties: { enableLoadingPhrases: { type: 'boolean', label: 'Enable Loading Phrases', category: 'UI', requiresRestart: true, default: true, description: 'Enable loading phrases during operations.', showInDialog: true, }, }, }, }, }, tools: { type: 'object', label: 'Tools', category: 'Tools', requiresRestart: false, default: {}, description: 'Tool settings.', showInDialog: false, properties: { shell: { type: 'object', label: 'Shell', category: 'Tools', requiresRestart: false, default: {}, description: 'Shell tool settings.', showInDialog: false, properties: { pager: { type: 'string', label: 'Pager', category: 'Tools', requiresRestart: false, default: 'less', description: 'The pager to use for long output.', showInDialog: true, }, }, }, }, }, } as const satisfies SettingsSchema; vi.mocked(getSettingsSchema).mockReturnValue( SETTINGS_SCHEMA as unknown as SettingsSchemaType, ); }); afterEach(() => { TEST_ONLY.clearFlattenedSchema(); vi.clearAllMocks(); vi.resetAllMocks(); }); describe('Schema Utilities', () => { describe('getSettingsByCategory', () => { it('should group settings by category', () => { const categories = getSettingsByCategory(); expect(categories).toHaveProperty('Advanced'); expect(categories).toHaveProperty('Basic'); }); it('should include key property in grouped settings', () => { const categories = getSettingsByCategory(); Object.entries(categories).forEach(([_category, settings]) => { settings.forEach((setting) => { expect(setting.key).toBeDefined(); }); }); }); }); describe('getSettingDefinition', () => { it('should return definition for valid setting', () => { const definition = getSettingDefinition('ui.theme'); expect(definition).toBeDefined(); expect(definition?.label).toBe('Theme'); }); it('should return undefined for invalid setting', () => { const definition = getSettingDefinition('invalidSetting'); expect(definition).toBeUndefined(); }); }); describe('requiresRestart', () => { it('should return true for settings that require restart', () => { expect(requiresRestart('ui.requiresRestart')).toBe(true); }); it('should return false for settings that do not require restart', () => { expect(requiresRestart('ui.theme')).toBe(false); }); it('should return false for invalid settings', () => { expect(requiresRestart('invalidSetting')).toBe(false); }); }); describe('getDefaultValue', () => { it('should return correct default values', () => { expect(getDefaultValue('test')).toBe('hello'); expect(getDefaultValue('ui.requiresRestart')).toBe(false); }); it('should return undefined for invalid settings', () => { expect(getDefaultValue('invalidSetting')).toBeUndefined(); }); }); describe('getRestartRequiredSettings', () => { it('should return all settings that require restart', () => { const restartSettings = getRestartRequiredSettings(); expect(restartSettings).toContain('mcpServers'); expect(restartSettings).toContain('ui.requiresRestart'); }); }); describe('getEffectiveValue', () => { it('should return value from settings when set', () => { const settings = makeMockSettings({ ui: { requiresRestart: true } }); const value = getEffectiveValue('ui.requiresRestart', settings); expect(value).toBe(true); }); it('should return default value when not set anywhere', () => { const settings = makeMockSettings({}); const value = getEffectiveValue('ui.requiresRestart', settings); expect(value).toBe(false); // default value }); it('should handle nested settings correctly', () => { const settings = makeMockSettings({ ui: { accessibility: { enableLoadingPhrases: false } }, }); const value = getEffectiveValue( 'ui.accessibility.enableLoadingPhrases', settings, ); expect(value).toBe(false); }); it('should return undefined for invalid settings', () => { const settings = makeMockSettings({}); const value = getEffectiveValue('invalidSetting', settings); expect(value).toBeUndefined(); }); }); describe('getAllSettingKeys', () => { it('should return all setting keys', () => { const keys = getAllSettingKeys(); expect(keys).toContain('test'); expect(keys).toContain('ui.accessibility.enableLoadingPhrases'); }); }); describe('getSettingsByType', () => { it('should return only boolean settings', () => { const booleanSettings = getSettingsByType('boolean'); expect(booleanSettings.length).toBeGreaterThan(0); booleanSettings.forEach((setting) => { expect(setting.type).toBe('boolean'); }); }); }); describe('getSettingsRequiringRestart', () => { it('should return only settings that require restart', () => { const restartSettings = getSettingsRequiringRestart(); expect(restartSettings.length).toBeGreaterThan(0); restartSettings.forEach((setting) => { expect(setting.requiresRestart).toBe(true); }); }); }); describe('isValidSettingKey', () => { it('should return true for valid setting keys', () => { expect(isValidSettingKey('ui.requiresRestart')).toBe(true); expect(isValidSettingKey('ui.accessibility.enableLoadingPhrases')).toBe( true, ); }); it('should return false for invalid setting keys', () => { expect(isValidSettingKey('invalidSetting')).toBe(false); expect(isValidSettingKey('')).toBe(false); }); }); describe('getSettingCategory', () => { it('should return correct category for valid settings', () => { expect(getSettingCategory('ui.requiresRestart')).toBe('UI'); expect( getSettingCategory('ui.accessibility.enableLoadingPhrases'), ).toBe('UI'); }); it('should return undefined for invalid settings', () => { expect(getSettingCategory('invalidSetting')).toBeUndefined(); }); }); describe('shouldShowInDialog', () => { it('should return true for settings marked to show in dialog', () => { expect(shouldShowInDialog('ui.requiresRestart')).toBe(true); expect(shouldShowInDialog('general.vimMode')).toBe(true); expect(shouldShowInDialog('ui.hideWindowTitle')).toBe(true); }); it('should return false for settings marked to hide from dialog', () => { expect(shouldShowInDialog('ui.theme')).toBe(false); }); it('should return true for invalid settings (default behavior)', () => { expect(shouldShowInDialog('invalidSetting')).toBe(true); }); }); describe('getDialogSettingsByCategory', () => { it('should only return settings marked for dialog display', async () => { const categories = getDialogSettingsByCategory(); // Should include UI settings that are marked for dialog expect(categories['UI']).toBeDefined(); const uiSettings = categories['UI']; const uiKeys = uiSettings.map((s) => s.key); expect(uiKeys).toContain('ui.requiresRestart'); expect(uiKeys).toContain('ui.accessibility.enableLoadingPhrases'); expect(uiKeys).not.toContain('ui.theme'); // This is now marked false }); it('should include Advanced category settings', () => { const categories = getDialogSettingsByCategory(); // Advanced settings should now be included because of autoConfigureMemory expect(categories['Advanced']).toBeDefined(); const advancedSettings = categories['Advanced']; expect(advancedSettings.map((s) => s.key)).toContain( 'advanced.autoConfigureMemory', ); }); it('should include settings with showInDialog=true', () => { const categories = getDialogSettingsByCategory(); const allSettings = Object.values(categories).flat(); const allKeys = allSettings.map((s) => s.key); expect(allKeys).toContain('test'); expect(allKeys).toContain('ui.requiresRestart'); expect(allKeys).not.toContain('ui.theme'); // Now hidden expect(allKeys).not.toContain('general.preferredEditor'); // Now hidden }); }); describe('getDialogSettingsByType', () => { it('should return only boolean dialog settings', () => { const booleanSettings = getDialogSettingsByType('boolean'); const keys = booleanSettings.map((s) => s.key); expect(keys).toContain('ui.requiresRestart'); expect(keys).toContain('ui.accessibility.enableLoadingPhrases'); expect(keys).not.toContain('privacy.usageStatisticsEnabled'); expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting expect(keys).not.toContain('security.auth.useExternal'); // Advanced setting }); it('should return only string dialog settings', () => { const stringSettings = getDialogSettingsByType('string'); const keys = stringSettings.map((s) => s.key); // Note: theme and preferredEditor are now hidden from dialog expect(keys).not.toContain('ui.theme'); // Now marked false expect(keys).not.toContain('general.preferredEditor'); // Now marked false expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting // Check that user-facing tool settings are included expect(keys).toContain('tools.shell.pager'); // Check that advanced/hidden tool settings are excluded expect(keys).not.toContain('tools.discoveryCommand'); expect(keys).not.toContain('tools.callCommand'); expect(keys.every((key) => !key.startsWith('advanced.'))).toBe(true); }); }); describe('getDialogSettingKeys', () => { it('should return only settings marked for dialog display', () => { const dialogKeys = getDialogSettingKeys(); // Should include settings marked for dialog expect(dialogKeys).toContain('ui.requiresRestart'); // Should include nested settings marked for dialog expect(dialogKeys).toContain('ui.accessibility.enableLoadingPhrases'); // Should NOT include settings marked as hidden expect(dialogKeys).not.toContain('ui.theme'); // Hidden }); it('should return fewer keys than getAllSettingKeys', () => { const allKeys = getAllSettingKeys(); const dialogKeys = getDialogSettingKeys(); expect(dialogKeys.length).toBeLessThan(allKeys.length); expect(dialogKeys.length).toBeGreaterThan(0); }); const nestedDialogKey = 'context.fileFiltering.respectGitIgnore'; function mockNestedDialogSchema() { vi.mocked(getSettingsSchema).mockReturnValue({ context: { type: 'object', label: 'Context', category: 'Context', requiresRestart: false, default: {}, description: 'Settings for managing context provided to the model.', showInDialog: false, properties: { fileFiltering: { type: 'object', label: 'File Filtering', category: 'Context', requiresRestart: true, default: {}, description: 'Settings for git-aware file filtering.', showInDialog: false, properties: { respectGitIgnore: { type: 'boolean', label: 'Respect .gitignore', category: 'Context', requiresRestart: true, default: true, description: 'Respect .gitignore files when searching', showInDialog: true, }, }, }, }, }, } as unknown as SettingsSchemaType); } it('should include nested file filtering setting in dialog keys', () => { mockNestedDialogSchema(); const dialogKeys = getDialogSettingKeys(); expect(dialogKeys).toContain(nestedDialogKey); }); }); }); describe('Business Logic Utilities', () => { describe('isInSettingsScope', () => { it('should return true for top-level settings that exist', () => { const settings = makeMockSettings({ ui: { requiresRestart: true } }); expect(isInSettingsScope('ui.requiresRestart', settings)).toBe(true); }); it('should return false for top-level settings that do not exist', () => { const settings = makeMockSettings({}); expect(isInSettingsScope('ui.requiresRestart', settings)).toBe(false); }); it('should return true for nested settings that exist', () => { const settings = makeMockSettings({ ui: { accessibility: { enableLoadingPhrases: true } }, }); expect( isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings), ).toBe(true); }); it('should return false for nested settings that do not exist', () => { const settings = makeMockSettings({}); expect( isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings), ).toBe(false); }); it('should return false when parent exists but child does not', () => { const settings = makeMockSettings({ ui: { accessibility: {} } }); expect( isInSettingsScope('ui.accessibility.enableLoadingPhrases', settings), ).toBe(false); }); }); describe('getDisplayValue', () => { describe('enum behavior', () => { enum StringEnum { FOO = 'foo', BAR = 'bar', BAZ = 'baz', } enum NumberEnum { ONE = 1, TWO = 2, THREE = 3, } const SETTING: SettingDefinition = { type: 'enum', label: 'Theme', options: [ { value: StringEnum.FOO, label: 'Foo', }, { value: StringEnum.BAR, label: 'Bar', }, { value: StringEnum.BAZ, label: 'Baz', }, ], category: 'UI', requiresRestart: false, default: StringEnum.BAR, description: 'The color theme for the UI.', showInDialog: false, }; it('handles display of number-based enums', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { theme: { ...SETTING, options: [ { value: NumberEnum.ONE, label: 'One', }, { value: NumberEnum.TWO, label: 'Two', }, { value: NumberEnum.THREE, label: 'Three', }, ], }, }, }, } as unknown as SettingsSchemaType); const settings = makeMockSettings({ ui: { theme: NumberEnum.THREE }, }); const mergedSettings = makeMockSettings({ ui: { theme: NumberEnum.THREE }, }); const result = getDisplayValue('ui.theme', settings, mergedSettings); expect(result).toBe('Three*'); }); it('handles default values for number-based enums', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { theme: { ...SETTING, default: NumberEnum.THREE, options: [ { value: NumberEnum.ONE, label: 'One', }, { value: NumberEnum.TWO, label: 'Two', }, { value: NumberEnum.THREE, label: 'Three', }, ], }, }, }, } as unknown as SettingsSchemaType); const result = getDisplayValue( 'ui.theme', makeMockSettings({}), makeMockSettings({}), ); expect(result).toBe('Three'); }); it('shows the enum display value', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { theme: { ...SETTING } } }, } as unknown as SettingsSchemaType); const settings = makeMockSettings({ ui: { theme: StringEnum.BAR } }); const mergedSettings = makeMockSettings({ ui: { theme: StringEnum.BAR }, }); const result = getDisplayValue('ui.theme', settings, mergedSettings); expect(result).toBe('Bar*'); }); it('passes through unknown values verbatim', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { theme: { ...SETTING }, }, }, } as unknown as SettingsSchemaType); const settings = makeMockSettings({ ui: { theme: 'xyz' } }); const mergedSettings = makeMockSettings({ ui: { theme: 'xyz' } }); const result = getDisplayValue('ui.theme', settings, mergedSettings); expect(result).toBe('xyz*'); }); it('shows the default value for string enums', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { theme: { ...SETTING, default: StringEnum.BAR }, }, }, } as unknown as SettingsSchemaType); const result = getDisplayValue( 'ui.theme', makeMockSettings({}), makeMockSettings({}), ); expect(result).toBe('Bar'); }); }); it('should show value with * when setting exists in scope', () => { const settings = makeMockSettings({ ui: { requiresRestart: true } }); const mergedSettings = makeMockSettings({ ui: { requiresRestart: true }, }); const result = getDisplayValue( 'ui.requiresRestart', settings, mergedSettings, ); expect(result).toBe('true*'); }); it('should not show * when key is not in scope', () => { const settings = makeMockSettings({}); // no setting in scope const mergedSettings = makeMockSettings({ ui: { requiresRestart: false }, }); const result = getDisplayValue( 'ui.requiresRestart', settings, mergedSettings, ); expect(result).toBe('false'); // shows default value }); it('should show value with * when setting exists in scope, even when it matches default', () => { const settings = makeMockSettings({ ui: { requiresRestart: false }, }); // false matches default, but key is explicitly set in scope const mergedSettings = makeMockSettings({ ui: { requiresRestart: false }, }); const result = getDisplayValue( 'ui.requiresRestart', settings, mergedSettings, ); expect(result).toBe('false*'); }); it('should show schema default (not inherited merged value) when key is not in scope', () => { const settings = makeMockSettings({}); // no setting in current scope const mergedSettings = makeMockSettings({ ui: { requiresRestart: true }, }); // inherited merged value differs from schema default (false) const result = getDisplayValue( 'ui.requiresRestart', settings, mergedSettings, ); expect(result).toBe('false'); }); it('should display objects as JSON strings, not "[object Object]"', () => { vi.mocked(getSettingsSchema).mockReturnValue({ experimental: { type: 'object', label: 'Experimental', category: 'Experimental', requiresRestart: true, default: {}, description: 'Experimental settings', showInDialog: false, properties: { gemmaModelRouter: { type: 'object', label: 'Gemma Model Router', category: 'Experimental', requiresRestart: true, default: {}, description: 'Gemma model router settings', showInDialog: true, }, }, }, } as unknown as SettingsSchemaType); // Test with empty object (default) const emptySettings = makeMockSettings({}); const emptyResult = getDisplayValue( 'experimental.gemmaModelRouter', emptySettings, emptySettings, ); expect(emptyResult).toBe('{}'); expect(emptyResult).not.toBe('[object Object]'); // Test with object containing values const settings = makeMockSettings({ experimental: { gemmaModelRouter: { enabled: true, host: 'localhost' }, }, }); const result = getDisplayValue( 'experimental.gemmaModelRouter', settings, settings, ); expect(result).toBe('{"enabled":true,"host":"localhost"}*'); expect(result).not.toContain('[object Object]'); }); }); describe('getDisplayValue with units', () => { it('should format percentage correctly when unit is %', () => { vi.mocked(getSettingsSchema).mockReturnValue({ model: { properties: { compressionThreshold: { type: 'number', label: 'Context Compression Threshold', category: 'Model', requiresRestart: true, default: 0.5, unit: '%', }, }, }, } as unknown as SettingsSchemaType); const settings = makeMockSettings({ model: { compressionThreshold: 0.8 }, }); const result = getDisplayValue( 'model.compressionThreshold', settings, makeMockSettings({}), ); expect(result).toBe('0.8 (80%)*'); }); it('should append unit for non-% units', () => { vi.mocked(getSettingsSchema).mockReturnValue({ ui: { properties: { pollingInterval: { type: 'number', label: 'Polling Interval', category: 'UI', requiresRestart: false, default: 60, unit: 's', }, }, }, } as unknown as SettingsSchemaType); const settings = makeMockSettings({ ui: { pollingInterval: 30 } }); const result = getDisplayValue( 'ui.pollingInterval', settings, makeMockSettings({}), ); expect(result).toBe('30s*'); }); }); }); });