refactor: refactor settings to a nested structure (#7244)

This commit is contained in:
Gal Zahavi
2025-08-27 18:39:45 -07:00
committed by GitHub
parent b8a7bfd136
commit f22263c9e8
41 changed files with 2852 additions and 1424 deletions

View File

@@ -333,7 +333,7 @@ describe('loadCliConfig', () => {
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { showMemoryUsage: false };
const settings: Settings = { ui: { showMemoryUsage: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getShowMemoryUsage()).toBe(false);
});
@@ -341,7 +341,7 @@ describe('loadCliConfig', () => {
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
process.argv = ['node', 'script.js', '--show-memory-usage'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { showMemoryUsage: false };
const settings: Settings = { ui: { showMemoryUsage: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getShowMemoryUsage()).toBe(true);
});
@@ -804,7 +804,7 @@ describe('mergeExcludeTools', () => {
});
it('should merge excludeTools from settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -840,7 +840,7 @@ describe('mergeExcludeTools', () => {
});
it('should handle overlapping excludeTools between settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -867,7 +867,7 @@ describe('mergeExcludeTools', () => {
});
it('should handle overlapping excludeTools between extensions', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const settings: Settings = { tools: { exclude: ['tool1'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -935,7 +935,7 @@ describe('mergeExcludeTools', () => {
it('should handle settings with excludeTools but no extensions', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
@@ -977,7 +977,7 @@ describe('mergeExcludeTools', () => {
});
it('should not modify the original settings object', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const settings: Settings = { tools: { exclude: ['tool1'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext',
@@ -1166,7 +1166,7 @@ describe('Approval mode tool exclusion logic', () => {
'test',
];
const argv = await parseArguments({} as Settings);
const settings: Settings = { excludeTools: ['custom_tool'] };
const settings: Settings = { tools: { exclude: ['custom_tool'] } };
const extensions: Extension[] = [];
const config = await loadCliConfig(
@@ -1297,7 +1297,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
allowMCPServers: ['server1', 'server2'],
mcp: { allowed: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1311,7 +1311,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1', 'server2'],
mcp: { excluded: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1324,8 +1324,10 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1'],
allowMCPServers: ['server1', 'server2'],
mcp: {
excluded: ['server1'],
allowed: ['server1', 'server2'],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1343,14 +1345,40 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1'],
allowMCPServers: ['server2'],
mcp: {
excluded: ['server1'],
allowed: ['server2'],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
});
});
it('should prioritize CLI flag over both allowed and excluded settings', async () => {
process.argv = [
'node',
'script.js',
'--allowed-mcp-server-names',
'server2',
'--allowed-mcp-server-names',
'server3',
];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
mcp: {
allowed: ['server1', 'server2'], // Should be ignored
excluded: ['server3'], // Should be ignored
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server2: { url: 'http://localhost:8081' },
server3: { url: 'http://localhost:8082' },
});
});
});
describe('loadCliConfig extensions', () => {
@@ -1403,7 +1431,9 @@ describe('loadCliConfig model selection', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{
model: 'gemini-9001-ultra',
model: {
name: 'gemini-9001-ultra',
},
},
[],
'test-session',
@@ -1433,7 +1463,9 @@ describe('loadCliConfig model selection', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{
model: 'gemini-9001-ultra',
model: {
name: 'gemini-9001-ultra',
},
},
[],
'test-session',
@@ -1485,7 +1517,9 @@ describe('loadCliConfig folderTrustFeature', () => {
it('should be true when settings.folderTrustFeature is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true };
const settings: Settings = {
security: { folderTrust: { featureEnabled: true } },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrustFeature()).toBe(true);
});
@@ -1509,8 +1543,12 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is false and folderTrust is false', async () => {
process.argv = ['node', 'script.js'];
const settings: Settings = {
folderTrustFeature: false,
folderTrust: false,
security: {
folderTrust: {
featureEnabled: false,
enabled: false,
},
},
};
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, [], 'test-session', argv);
@@ -1520,7 +1558,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: true,
enabled: false,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(false);
});
@@ -1528,7 +1573,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is false and folderTrust is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: false,
enabled: true,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(false);
});
@@ -1536,7 +1588,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be true when folderTrustFeature is true and folderTrust is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: true,
enabled: true,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(true);
});
@@ -1570,11 +1629,13 @@ describe('loadCliConfig with includeDirectories', () => {
];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
includeDirectories: [
path.resolve(path.sep, 'settings', 'path1'),
path.join(os.homedir(), 'settings', 'path2'),
path.join(mockCwd, 'settings', 'path3'),
],
context: {
includeDirectories: [
path.resolve(path.sep, 'settings', 'path1'),
path.join(os.homedir(), 'settings', 'path2'),
path.join(mockCwd, 'settings', 'path3'),
],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
const expected = [
@@ -1613,8 +1674,10 @@ describe('loadCliConfig chatCompression', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
chatCompression: {
contextPercentageThreshold: 0.5,
model: {
chatCompression: {
contextPercentageThreshold: 0.5,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
@@ -1658,7 +1721,7 @@ describe('loadCliConfig useRipgrep', () => {
it('should be true when useRipgrep is set to true in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { useRipgrep: true };
const settings: Settings = { tools: { useRipgrep: true } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getUseRipgrep()).toBe(true);
});
@@ -1666,7 +1729,7 @@ describe('loadCliConfig useRipgrep', () => {
it('should be false when useRipgrep is explicitly set to false in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { useRipgrep: false };
const settings: Settings = { tools: { useRipgrep: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getUseRipgrep()).toBe(false);
});
@@ -2002,13 +2065,26 @@ describe('loadCliConfig trustedFolder', () => {
} of testCases) {
it(`should be correct for: ${description}`, async () => {
(isWorkspaceTrusted as Mock).mockImplementation((settings: Settings) => {
const featureIsEnabled =
(settings.folderTrustFeature ?? false) &&
(settings.folderTrust ?? true);
return featureIsEnabled ? mockTrustValue : true;
const folderTrustFeature =
settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting =
settings.security?.folderTrust?.enabled ?? true;
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
if (!folderTrustEnabled) {
return true;
}
return mockTrustValue; // This is the part that comes from the test case
});
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature, folderTrust };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: folderTrustFeature,
enabled: folderTrust,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(expectedFolderTrust);

View File

@@ -244,7 +244,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
// Register MCP subcommands
.command(mcpCommand);
if (settings?.extensionManagement ?? false) {
if (settings?.experimental?.extensionManagement ?? false) {
yargsInstance.command(extensionsCommand);
}
@@ -311,7 +311,7 @@ export async function loadHierarchicalGeminiMemory(
extensionContextFilePaths,
memoryImportFormat,
fileFilteringOptions,
settings.memoryDiscoveryMaxDirs,
settings.context?.discoveryMaxDirs,
);
}
@@ -328,12 +328,13 @@ export async function loadCliConfig(
(v) => v === 'true' || v === '1',
) ||
false;
const memoryImportFormat = settings.memoryImportFormat || 'tree';
const memoryImportFormat = settings.context?.importFormat || 'tree';
const ideMode = settings.ideMode ?? false;
const ideMode = settings.ide?.enabled ?? false;
const folderTrustFeature = settings.folderTrustFeature ?? false;
const folderTrustSetting = settings.folderTrust ?? true;
const folderTrustFeature =
settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
const folderTrust = folderTrustFeature && folderTrustSetting;
const trustedFolder = isWorkspaceTrusted(settings);
@@ -351,8 +352,8 @@ export async function loadCliConfig(
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
if (settings.contextFileName) {
setServerGeminiMdFilename(settings.contextFileName);
if (settings.context?.fileName) {
setServerGeminiMdFilename(settings.context.fileName);
} else {
// Reset to default if not provided in settings.
setServerGeminiMdFilename(getCurrentGeminiMdFilename());
@@ -366,17 +367,19 @@ export async function loadCliConfig(
const fileFiltering = {
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
...settings.fileFiltering,
...settings.context?.fileFiltering,
};
const includeDirectories = (settings.includeDirectories || [])
const includeDirectories = (settings.context?.includeDirectories || [])
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
cwd,
settings.loadMemoryFromIncludeDirectories ? includeDirectories : [],
settings.context?.loadMemoryFromIncludeDirectories
? includeDirectories
: [],
debugMode,
fileService,
settings,
@@ -452,16 +455,16 @@ export async function loadCliConfig(
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
if (!argv.allowedMcpServerNames) {
if (settings.allowMCPServers) {
if (settings.mcp?.allowed) {
mcpServers = allowedMcpServers(
mcpServers,
settings.allowMCPServers,
settings.mcp.allowed,
blockedMcpServers,
);
}
if (settings.excludeMCPServers) {
const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean));
if (settings.mcp?.excluded) {
const excludedNames = new Set(settings.mcp.excluded.filter(Boolean));
if (excludedNames.size > 0) {
mcpServers = Object.fromEntries(
Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)),
@@ -482,7 +485,7 @@ export async function loadCliConfig(
// The screen reader argument takes precedence over the accessibility setting.
const screenReader =
argv.screenReader ?? settings.accessibility?.screenReader ?? false;
argv.screenReader ?? settings.ui?.accessibility?.screenReader ?? false;
return new Config({
sessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -490,23 +493,24 @@ export async function loadCliConfig(
targetDir: cwd,
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.loadMemoryFromIncludeDirectories || false,
settings.context?.loadMemoryFromIncludeDirectories || false,
debugMode,
question,
fullContext: argv.allFiles || false,
coreTools: settings.coreTools || undefined,
allowedTools: argv.allowedTools || settings.allowedTools || undefined,
coreTools: settings.tools?.core || undefined,
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
excludeTools,
toolDiscoveryCommand: settings.toolDiscoveryCommand,
toolCallCommand: settings.toolCallCommand,
mcpServerCommand: settings.mcpServerCommand,
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: settings.mcp?.serverCommand,
mcpServers,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
approvalMode,
showMemoryUsage: argv.showMemoryUsage || settings.showMemoryUsage || false,
showMemoryUsage:
argv.showMemoryUsage || settings.ui?.showMemoryUsage || false,
accessibility: {
...settings.accessibility,
...settings.ui?.accessibility,
screenReader,
},
telemetry: {
@@ -525,16 +529,17 @@ export async function loadCliConfig(
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
},
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
// Git-aware file filtering settings
fileFiltering: {
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore,
respectGitIgnore: settings.context?.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: settings.context?.fileFiltering?.respectGeminiIgnore,
enableRecursiveFileSearch:
settings.fileFiltering?.enableRecursiveFileSearch,
disableFuzzySearch: settings.fileFiltering?.disableFuzzySearch,
settings.context?.fileFiltering?.enableRecursiveFileSearch,
disableFuzzySearch: settings.context?.fileFiltering?.disableFuzzySearch,
},
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
checkpointing:
argv.checkpointing || settings.general?.checkpointing?.enabled,
proxy:
argv.proxy ||
process.env['HTTPS_PROXY'] ||
@@ -543,26 +548,26 @@ export async function loadCliConfig(
process.env['http_proxy'],
cwd,
fileDiscoveryService: fileService,
bugCommand: settings.bugCommand,
model: argv.model || settings.model || DEFAULT_GEMINI_MODEL,
bugCommand: settings.advanced?.bugCommand,
model: argv.model || settings.model?.name || DEFAULT_GEMINI_MODEL,
extensionContextFilePaths,
maxSessionTurns: settings.maxSessionTurns ?? -1,
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,
blockedMcpServers,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.summarizeToolOutput,
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
chatCompression: settings.chatCompression,
chatCompression: settings.model?.chatCompression,
folderTrustFeature,
folderTrust,
interactive,
trustedFolder,
useRipgrep: settings.useRipgrep,
shouldUseNodePtyShell: settings.shouldUseNodePtyShell,
skipNextSpeakerCheck: settings.skipNextSpeakerCheck,
enablePromptCompletion: settings.enablePromptCompletion ?? false,
useRipgrep: settings.tools?.useRipgrep,
shouldUseNodePtyShell: settings.tools?.usePty,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
});
}
@@ -624,7 +629,7 @@ function mergeExcludeTools(
extraExcludes?: string[] | undefined,
): string[] {
const allExcludeTools = new Set([
...(settings.excludeTools || []),
...(settings.tools?.exclude || []),
...(extraExcludes || []),
]);
for (const extension of extensions) {

View File

@@ -112,7 +112,7 @@ export function loadExtensions(workspaceDir: string): Extension[] {
const disabledExtensions = settings.extensions?.disabled ?? [];
const allExtensions = [...loadUserExtensions()];
if (!settings.extensionManagement) {
if (!settings.experimental?.extensionManagement) {
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
}

View File

@@ -92,7 +92,7 @@ export async function loadSandboxConfig(
settings: Settings,
argv: SandboxCliArgs,
): Promise<SandboxConfig | undefined> {
const sandboxOption = argv.sandbox ?? settings.sandbox;
const sandboxOption = argv.sandbox ?? settings.tools?.sandbox;
const command = getSandboxCommand(sandboxOption);
const packageJson = await getPackageJson();

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import type { Settings, MemoryImportFormat } from './settingsSchema.js';
import { mergeWith } from 'lodash-es';
export type { Settings, MemoryImportFormat };
@@ -27,6 +28,58 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
const MIGRATE_V2_OVERWRITE = false;
// As defined in spec.md
const MIGRATION_MAP: Record<string, string> = {
preferredEditor: 'general.preferredEditor',
vimMode: 'general.vimMode',
disableAutoUpdate: 'general.disableAutoUpdate',
disableUpdateNag: 'general.disableUpdateNag',
checkpointing: 'general.checkpointing',
theme: 'ui.theme',
customThemes: 'ui.customThemes',
hideWindowTitle: 'ui.hideWindowTitle',
hideTips: 'ui.hideTips',
hideBanner: 'ui.hideBanner',
hideFooter: 'ui.hideFooter',
showMemoryUsage: 'ui.showMemoryUsage',
showLineNumbers: 'ui.showLineNumbers',
accessibility: 'ui.accessibility',
ideMode: 'ide.enabled',
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
telemetry: 'telemetry',
model: 'model.name',
maxSessionTurns: 'model.maxSessionTurns',
summarizeToolOutput: 'model.summarizeToolOutput',
chatCompression: 'model.chatCompression',
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
contextFileName: 'context.fileName',
memoryImportFormat: 'context.importFormat',
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
includeDirectories: 'context.includeDirectories',
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
fileFiltering: 'context.fileFiltering',
sandbox: 'tools.sandbox',
shouldUseNodePtyShell: 'tools.usePty',
coreTools: 'tools.core',
excludeTools: 'tools.exclude',
toolDiscoveryCommand: 'tools.discoveryCommand',
toolCallCommand: 'tools.callCommand',
mcpServerCommand: 'mcp.serverCommand',
allowMCPServers: 'mcp.allowed',
excludeMCPServers: 'mcp.excluded',
folderTrustFeature: 'security.folderTrust.featureEnabled',
folderTrust: 'security.folderTrust.enabled',
selectedAuthType: 'security.auth.selectedType',
useExternalAuth: 'security.auth.useExternal',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
excludedProjectEnvVars: 'advanced.excludedEnvVars',
bugCommand: 'advanced.bugCommand',
};
export function getSystemSettingsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
@@ -82,6 +135,134 @@ export interface SettingsFile {
path: string;
}
function setNestedProperty(
obj: Record<string, unknown>,
path: string,
value: unknown,
) {
const keys = path.split('.');
const lastKey = keys.pop();
if (!lastKey) return;
let current: Record<string, unknown> = obj;
for (const key of keys) {
if (current[key] === undefined) {
current[key] = {};
}
const next = current[key];
if (typeof next === 'object' && next !== null) {
current = next as Record<string, unknown>;
} else {
// This path is invalid, so we stop.
return;
}
}
current[lastKey] = value;
}
function needsMigration(settings: Record<string, unknown>): boolean {
return !('general' in settings);
}
function migrateSettingsToV2(
flatSettings: Record<string, unknown>,
): Record<string, unknown> | null {
if (!needsMigration(flatSettings)) {
return null;
}
const v2Settings: Record<string, unknown> = {};
const flatKeys = new Set(Object.keys(flatSettings));
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
if (flatKeys.has(oldKey)) {
setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
flatKeys.delete(oldKey);
}
}
// Preserve mcpServers at the top level
if (flatSettings['mcpServers']) {
v2Settings['mcpServers'] = flatSettings['mcpServers'];
flatKeys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of flatKeys) {
v2Settings[remainingKey] = flatSettings[remainingKey];
}
return v2Settings;
}
function getNestedProperty(
obj: Record<string, unknown>,
path: string,
): unknown {
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return undefined;
}
current = (current as Record<string, unknown>)[key];
}
return current;
}
const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
);
// Dynamically determine the top-level keys from the V2 settings structure.
const KNOWN_V2_CONTAINERS = new Set(
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
);
export function migrateSettingsToV1(
v2Settings: Record<string, unknown>,
): Record<string, unknown> {
const v1Settings: Record<string, unknown> = {};
const v2Keys = new Set(Object.keys(v2Settings));
for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
const value = getNestedProperty(v2Settings, newPath);
if (value !== undefined) {
v1Settings[oldKey] = value;
v2Keys.delete(newPath.split('.')[0]);
}
}
// Preserve mcpServers at the top level
if (v2Settings['mcpServers']) {
v1Settings['mcpServers'] = v2Settings['mcpServers'];
v2Keys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of v2Keys) {
const value = v2Settings[remainingKey];
if (value === undefined) {
continue;
}
// Don't carry over empty objects that were just containers for migrated settings.
if (
KNOWN_V2_CONTAINERS.has(remainingKey) &&
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).length === 0
) {
continue;
}
v1Settings[remainingKey] = value;
}
return v1Settings;
}
function mergeSettings(
system: Settings,
systemDefaults: Settings,
@@ -92,8 +273,17 @@ function mergeSettings(
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
// folderTrust is not supported at workspace level.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { folderTrust, ...safeWorkspaceWithoutFolderTrust } = safeWorkspace;
const { security, ...restOfWorkspace } = safeWorkspace;
const safeWorkspaceWithoutFolderTrust = security
? {
...restOfWorkspace,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
security: (({ folderTrust, ...rest }) => rest)(security),
}
: {
...restOfWorkspace,
security: {},
};
// Settings are merged with the following precedence (last one wins for
// single values):
@@ -109,40 +299,84 @@ function mergeSettings(
...user,
...safeWorkspaceWithoutFolderTrust,
...system,
customThemes: {
...(systemDefaults.customThemes || {}),
...(user.customThemes || {}),
...(safeWorkspace.customThemes || {}),
...(system.customThemes || {}),
ui: {
...(systemDefaults.ui || {}),
...(user.ui || {}),
...(safeWorkspaceWithoutFolderTrust.ui || {}),
...(system.ui || {}),
customThemes: {
...(systemDefaults.ui?.customThemes || {}),
...(user.ui?.customThemes || {}),
...(safeWorkspaceWithoutFolderTrust.ui?.customThemes || {}),
...(system.ui?.customThemes || {}),
},
},
security: {
...(systemDefaults.security || {}),
...(user.security || {}),
...(safeWorkspaceWithoutFolderTrust.security || {}),
...(system.security || {}),
},
mcp: {
...(systemDefaults.mcp || {}),
...(user.mcp || {}),
...(safeWorkspaceWithoutFolderTrust.mcp || {}),
...(system.mcp || {}),
},
mcpServers: {
...(systemDefaults.mcpServers || {}),
...(user.mcpServers || {}),
...(safeWorkspace.mcpServers || {}),
...(safeWorkspaceWithoutFolderTrust.mcpServers || {}),
...(system.mcpServers || {}),
},
includeDirectories: [
...(systemDefaults.includeDirectories || []),
...(user.includeDirectories || []),
...(safeWorkspace.includeDirectories || []),
...(system.includeDirectories || []),
],
chatCompression: {
...(systemDefaults.chatCompression || {}),
...(user.chatCompression || {}),
...(safeWorkspace.chatCompression || {}),
...(system.chatCompression || {}),
context: {
...(systemDefaults.context || {}),
...(user.context || {}),
...(safeWorkspaceWithoutFolderTrust.context || {}),
...(system.context || {}),
includeDirectories: [
...(systemDefaults.context?.includeDirectories || []),
...(user.context?.includeDirectories || []),
...(safeWorkspaceWithoutFolderTrust.context?.includeDirectories || []),
...(system.context?.includeDirectories || []),
],
},
model: {
...(systemDefaults.model || {}),
...(user.model || {}),
...(safeWorkspaceWithoutFolderTrust.model || {}),
...(system.model || {}),
chatCompression: {
...(systemDefaults.model?.chatCompression || {}),
...(user.model?.chatCompression || {}),
...(safeWorkspaceWithoutFolderTrust.model?.chatCompression || {}),
...(system.model?.chatCompression || {}),
},
},
advanced: {
...(systemDefaults.advanced || {}),
...(user.advanced || {}),
...(safeWorkspaceWithoutFolderTrust.advanced || {}),
...(system.advanced || {}),
excludedEnvVars: [
...new Set([
...(systemDefaults.advanced?.excludedEnvVars || []),
...(user.advanced?.excludedEnvVars || []),
...(safeWorkspaceWithoutFolderTrust.advanced?.excludedEnvVars || []),
...(system.advanced?.excludedEnvVars || []),
]),
],
},
extensions: {
...(systemDefaults.extensions || {}),
...(user.extensions || {}),
...(safeWorkspace.extensions || {}),
...(safeWorkspaceWithoutFolderTrust.extensions || {}),
...(system.extensions || {}),
disabled: [
...new Set([
...(systemDefaults.extensions?.disabled || []),
...(user.extensions?.disabled || []),
...(safeWorkspace.extensions?.disabled || []),
...(safeWorkspaceWithoutFolderTrust.extensions?.disabled || []),
...(system.extensions?.disabled || []),
]),
],
@@ -150,7 +384,8 @@ function mergeSettings(
...new Set([
...(systemDefaults.extensions?.workspacesWithMigrationNudge || []),
...(user.extensions?.workspacesWithMigrationNudge || []),
...(safeWorkspace.extensions?.workspacesWithMigrationNudge || []),
...(safeWorkspaceWithoutFolderTrust.extensions
?.workspacesWithMigrationNudge || []),
...(system.extensions?.workspacesWithMigrationNudge || []),
]),
],
@@ -166,6 +401,7 @@ export class LoadedSettings {
workspace: SettingsFile,
errors: SettingsError[],
isTrusted: boolean,
migratedInMemorScopes: Set<SettingScope>,
) {
this.system = system;
this.systemDefaults = systemDefaults;
@@ -173,6 +409,7 @@ export class LoadedSettings {
this.workspace = workspace;
this.errors = errors;
this.isTrusted = isTrusted;
this.migratedInMemorScopes = migratedInMemorScopes;
this._merged = this.computeMergedSettings();
}
@@ -182,6 +419,7 @@ export class LoadedSettings {
readonly workspace: SettingsFile;
readonly errors: SettingsError[];
readonly isTrusted: boolean;
readonly migratedInMemorScopes: Set<SettingScope>;
private _merged: Settings;
@@ -214,13 +452,9 @@ export class LoadedSettings {
}
}
setValue<K extends keyof Settings>(
scope: SettingScope,
key: K,
value: Settings[K],
): void {
setValue(scope: SettingScope, key: string, value: unknown): void {
const settingsFile = this.forScope(scope);
settingsFile.settings[key] = value;
setNestedProperty(settingsFile.settings, key, value);
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
}
@@ -357,7 +591,8 @@ export function loadEnvironment(settings?: Settings): void {
const parsedEnv = dotenv.parse(envFileContent);
const excludedVars =
resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
resolvedSettings?.advanced?.excludedEnvVars ||
DEFAULT_EXCLUDED_ENV_VARS;
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
for (const key in parsedEnv) {
@@ -391,6 +626,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
const settingsErrors: SettingsError[] = [];
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
const migratedInMemorScopes = new Set<SettingScope>();
// Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
@@ -411,85 +647,97 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
workspaceDir,
).getWorkspaceSettingsPath();
// Load system settings
try {
if (fs.existsSync(systemSettingsPath)) {
const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8');
systemSettings = JSON.parse(stripJsonComments(systemContent)) as Settings;
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemSettingsPath,
});
}
// Load system defaults
try {
if (fs.existsSync(systemDefaultsPath)) {
const systemDefaultsContent = fs.readFileSync(
systemDefaultsPath,
'utf-8',
);
const parsedSystemDefaults = JSON.parse(
stripJsonComments(systemDefaultsContent),
) as Settings;
systemDefaultSettings = resolveEnvVarsInObject(parsedSystemDefaults);
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemDefaultsPath,
});
}
// Load user settings
try {
if (fs.existsSync(USER_SETTINGS_PATH)) {
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
userSettings = JSON.parse(stripJsonComments(userContent)) as Settings;
// Support legacy theme names
if (userSettings.theme && userSettings.theme === 'VS') {
userSettings.theme = DefaultLight.name;
} else if (userSettings.theme && userSettings.theme === 'VS2015') {
userSettings.theme = DefaultDark.name;
}
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: USER_SETTINGS_PATH,
});
}
if (realWorkspaceDir !== realHomeDir) {
// Load workspace settings
const loadAndMigrate = (filePath: string, scope: SettingScope): Settings => {
try {
if (fs.existsSync(workspaceSettingsPath)) {
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
workspaceSettings = JSON.parse(
stripJsonComments(projectContent),
) as Settings;
if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
workspaceSettings.theme = DefaultLight.name;
} else if (
workspaceSettings.theme &&
workspaceSettings.theme === 'VS2015'
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
const rawSettings: unknown = JSON.parse(stripJsonComments(content));
if (
typeof rawSettings !== 'object' ||
rawSettings === null ||
Array.isArray(rawSettings)
) {
workspaceSettings.theme = DefaultDark.name;
settingsErrors.push({
message: 'Settings file is not a valid JSON object.',
path: filePath,
});
return {};
}
let settingsObject = rawSettings as Record<string, unknown>;
if (needsMigration(settingsObject)) {
console.error(`Legacy settings file detected at: ${filePath}`);
const migratedSettings = migrateSettingsToV2(settingsObject);
if (migratedSettings) {
if (MIGRATE_V2_OVERWRITE) {
try {
fs.renameSync(filePath, `${filePath}.orig`);
fs.writeFileSync(
filePath,
JSON.stringify(migratedSettings, null, 2),
'utf-8',
);
console.log(
`Successfully migrated and saved settings file: ${filePath}`,
);
} catch (e) {
console.error(
`Error migrating settings file on disk: ${getErrorMessage(
e,
)}`,
);
}
} else {
console.log(
`Successfully migrated settings for ${filePath} in-memory for the current session.`,
);
migratedInMemorScopes.add(scope);
}
settingsObject = migratedSettings;
}
}
return settingsObject as Settings;
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: workspaceSettingsPath,
path: filePath,
});
}
return {};
};
systemSettings = loadAndMigrate(systemSettingsPath, SettingScope.System);
systemDefaultSettings = loadAndMigrate(
systemDefaultsPath,
SettingScope.SystemDefaults,
);
userSettings = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
if (realWorkspaceDir !== realHomeDir) {
workspaceSettings = loadAndMigrate(
workspaceSettingsPath,
SettingScope.Workspace,
);
}
// Support legacy theme names
if (userSettings.ui?.theme === 'VS') {
userSettings.ui.theme = DefaultLight.name;
} else if (userSettings.ui?.theme === 'VS2015') {
userSettings.ui.theme = DefaultDark.name;
}
if (workspaceSettings.ui?.theme === 'VS') {
workspaceSettings.ui.theme = DefaultLight.name;
} else if (workspaceSettings.ui?.theme === 'VS2015') {
workspaceSettings.ui.theme = DefaultDark.name;
}
// For the initial trust check, we can only use user and system settings.
const initialTrustCheckSettings = { ...systemSettings, ...userSettings };
const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings) ?? true;
const initialTrustCheckSettings = mergeWith({}, systemSettings, userSettings);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings) ?? true;
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings(
@@ -529,21 +777,9 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
},
settingsErrors,
isTrusted,
migratedInMemorScopes,
);
// Validate chatCompression settings
const chatCompression = loadedSettings.merged.chatCompression;
const threshold = chatCompression?.contextPercentageThreshold;
if (
threshold != null &&
(typeof threshold !== 'number' || threshold < 0 || threshold > 1)
) {
console.warn(
`Invalid value for chatCompression.contextPercentageThreshold: "${threshold}". Please use a value between 0 and 1. Using default compression settings.`,
);
delete loadedSettings.merged.chatCompression;
}
return loadedSettings;
}
@@ -555,9 +791,16 @@ export function saveSettings(settingsFile: SettingsFile): void {
fs.mkdirSync(dirPath, { recursive: true });
}
let settingsToSave = settingsFile.settings;
if (!MIGRATE_V2_OVERWRITE) {
settingsToSave = migrateSettingsToV1(
settingsToSave as Record<string, unknown>,
) as Settings;
}
fs.writeFileSync(
settingsFile.path,
JSON.stringify(settingsFile.settings, null, 2),
JSON.stringify(settingsToSave, null, 2),
'utf-8',
);
} catch (error) {

View File

@@ -12,49 +12,18 @@ describe('SettingsSchema', () => {
describe('SETTINGS_SCHEMA', () => {
it('should contain all expected top-level settings', () => {
const expectedSettings = [
'theme',
'customThemes',
'showMemoryUsage',
'usageStatisticsEnabled',
'autoConfigureMaxOldSpaceSize',
'preferredEditor',
'maxSessionTurns',
'memoryImportFormat',
'memoryDiscoveryMaxDirs',
'contextFileName',
'vimMode',
'ideMode',
'accessibility',
'checkpointing',
'fileFiltering',
'disableAutoUpdate',
'hideWindowTitle',
'hideTips',
'hideBanner',
'selectedAuthType',
'useExternalAuth',
'sandbox',
'coreTools',
'excludeTools',
'toolDiscoveryCommand',
'toolCallCommand',
'mcpServerCommand',
'mcpServers',
'allowMCPServers',
'excludeMCPServers',
'general',
'ui',
'ide',
'privacy',
'telemetry',
'bugCommand',
'summarizeToolOutput',
'dnsResolutionOrder',
'excludedProjectEnvVars',
'disableUpdateNag',
'includeDirectories',
'loadMemoryFromIncludeDirectories',
'model',
'hasSeenIdeIntegrationNudge',
'folderTrustFeature',
'useRipgrep',
'debugKeystrokeLogging',
'context',
'tools',
'mcp',
'security',
'advanced',
];
expectedSettings.forEach((setting) => {
@@ -80,9 +49,16 @@ describe('SettingsSchema', () => {
it('should have correct nested setting structure', () => {
const nestedSettings = [
'accessibility',
'checkpointing',
'fileFiltering',
'general',
'ui',
'ide',
'privacy',
'model',
'context',
'tools',
'mcp',
'security',
'advanced',
];
nestedSettings.forEach((setting) => {
@@ -99,29 +75,36 @@ describe('SettingsSchema', () => {
it('should have accessibility nested properties', () => {
expect(
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases,
SETTINGS_SCHEMA.ui?.properties?.accessibility?.properties,
).toBeDefined();
expect(
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases.type,
SETTINGS_SCHEMA.ui?.properties?.accessibility.properties
?.disableLoadingPhrases.type,
).toBe('boolean');
});
it('should have checkpointing nested properties', () => {
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled).toBeDefined();
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled.type).toBe(
'boolean',
);
expect(
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled,
).toBeDefined();
expect(
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled
.type,
).toBe('boolean');
});
it('should have fileFiltering nested properties', () => {
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.respectGitIgnore,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.respectGitIgnore,
).toBeDefined();
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.respectGeminiIgnore,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.respectGeminiIgnore,
).toBeDefined();
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.enableRecursiveFileSearch,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.enableRecursiveFileSearch,
).toBeDefined();
});
@@ -150,11 +133,6 @@ describe('SettingsSchema', () => {
expect(categories.size).toBeGreaterThan(0);
expect(categories).toContain('General');
expect(categories).toContain('UI');
expect(categories).toContain('Mode');
expect(categories).toContain('Updates');
expect(categories).toContain('Accessibility');
expect(categories).toContain('Checkpointing');
expect(categories).toContain('File Filtering');
expect(categories).toContain('Advanced');
});
@@ -183,85 +161,148 @@ describe('SettingsSchema', () => {
it('should have showInDialog property configured', () => {
// Check that user-facing settings are marked for dialog display
expect(SETTINGS_SCHEMA.showMemoryUsage.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.vimMode.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.ideMode.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.disableAutoUpdate.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideWindowTitle.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideTips.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideBanner.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.usageStatisticsEnabled.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.ui.properties.showMemoryUsage.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.general.properties.vimMode.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ide.properties.enabled.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.general.properties.disableAutoUpdate.showInDialog,
).toBe(true);
expect(SETTINGS_SCHEMA.ui.properties.hideWindowTitle.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ui.properties.hideTips.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.ui.properties.hideBanner.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.privacy.properties.usageStatisticsEnabled.showInDialog,
).toBe(false);
// Check that advanced settings are hidden from dialog
expect(SETTINGS_SCHEMA.selectedAuthType.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.coreTools.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.security.properties.auth.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.tools.properties.core.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.mcpServers.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.telemetry.showInDialog).toBe(false);
// Check that some settings are appropriately hidden
expect(SETTINGS_SCHEMA.theme.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.customThemes.showInDialog).toBe(false); // Managed via theme editor
expect(SETTINGS_SCHEMA.checkpointing.showInDialog).toBe(false); // Experimental feature
expect(SETTINGS_SCHEMA.accessibility.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.fileFiltering.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.preferredEditor.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.autoConfigureMaxOldSpaceSize.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ui.properties.theme.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.ui.properties.customThemes.showInDialog).toBe(
false,
); // Managed via theme editor
expect(
SETTINGS_SCHEMA.general.properties.checkpointing.showInDialog,
).toBe(false); // Experimental feature
expect(SETTINGS_SCHEMA.ui.properties.accessibility.showInDialog).toBe(
false,
); // Changed to false
expect(
SETTINGS_SCHEMA.context.properties.fileFiltering.showInDialog,
).toBe(false); // Changed to false
expect(
SETTINGS_SCHEMA.general.properties.preferredEditor.showInDialog,
).toBe(false); // Changed to false
expect(
SETTINGS_SCHEMA.advanced.properties.autoConfigureMemory.showInDialog,
).toBe(false);
});
it('should infer Settings type correctly', () => {
// This test ensures that the Settings type is properly inferred from the schema
const settings: Settings = {
theme: 'dark',
includeDirectories: ['/path/to/dir'],
loadMemoryFromIncludeDirectories: true,
ui: {
theme: 'dark',
},
context: {
includeDirectories: ['/path/to/dir'],
loadMemoryFromIncludeDirectories: true,
},
};
// TypeScript should not complain about these properties
expect(settings.theme).toBe('dark');
expect(settings.includeDirectories).toEqual(['/path/to/dir']);
expect(settings.loadMemoryFromIncludeDirectories).toBe(true);
expect(settings.ui?.theme).toBe('dark');
expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']);
expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true);
});
it('should have includeDirectories setting in schema', () => {
expect(SETTINGS_SCHEMA.includeDirectories).toBeDefined();
expect(SETTINGS_SCHEMA.includeDirectories.type).toBe('array');
expect(SETTINGS_SCHEMA.includeDirectories.category).toBe('General');
expect(SETTINGS_SCHEMA.includeDirectories.default).toEqual([]);
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories,
).toBeDefined();
expect(SETTINGS_SCHEMA.context?.properties.includeDirectories.type).toBe(
'array',
);
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories.category,
).toBe('Context');
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories.default,
).toEqual([]);
});
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories).toBeDefined();
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.type).toBe(
'boolean',
);
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.category).toBe(
'General',
);
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.default).toBe(
false,
);
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories,
).toBeDefined();
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.category,
).toBe('Context');
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.default,
).toBe(false);
});
it('should have folderTrustFeature setting in schema', () => {
expect(SETTINGS_SCHEMA.folderTrustFeature).toBeDefined();
expect(SETTINGS_SCHEMA.folderTrustFeature.type).toBe('boolean');
expect(SETTINGS_SCHEMA.folderTrustFeature.category).toBe('General');
expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false);
expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled,
).toBeDefined();
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.category,
).toBe('Security');
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.default,
).toBe(false);
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.showInDialog,
).toBe(true);
});
it('should have debugKeystrokeLogging setting in schema', () => {
expect(SETTINGS_SCHEMA.debugKeystrokeLogging).toBeDefined();
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.type).toBe('boolean');
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.category).toBe('General');
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.default).toBe(false);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.requiresRestart).toBe(false);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.description).toBe(
'Enable debug logging of keystrokes to the console.',
);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging,
).toBeDefined();
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.category,
).toBe('General');
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.default,
).toBe(false);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging
.requiresRestart,
).toBe(false);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.showInDialog,
).toBe(true);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.description,
).toBe('Enable debug logging of keystrokes to the console.');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -132,8 +132,12 @@ describe('isWorkspaceTrusted', () => {
let mockCwd: string;
const mockRules: Record<string, TrustLevel> = {};
const mockSettings: Settings = {
folderTrustFeature: true,
folderTrust: true,
security: {
folderTrust: {
featureEnabled: true,
enabled: true,
},
},
};
beforeEach(() => {

View File

@@ -111,8 +111,9 @@ export function saveTrustedFolders(
}
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
const folderTrustFeature = settings.folderTrustFeature ?? false;
const folderTrustSetting = settings.folderTrust ?? true;
const folderTrustFeature =
settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
if (!folderTrustEnabled) {