mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-31 08:20:54 -07:00
Co-authored-by: Zheyuan <zlin252@emory.edu> Co-authored-by: Jacob Richman <jacob314@gmail.com>
843 lines
27 KiB
TypeScript
843 lines
27 KiB
TypeScript
/**
|
|
* @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<typeof import('../config/settingsSchema.js')>();
|
|
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<string, string>,
|
|
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*');
|
|
});
|
|
});
|
|
});
|
|
});
|