/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /// // Mock 'os' first. import * as osActual from 'node:os'; // Import for type info for the mock factory vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); return { ...actualOs, homedir: vi.fn(() => '/mock/home/user'), platform: vi.fn(() => 'linux'), }; }); // Mock trustedFolders vi.mock('./trustedFolders.js', () => ({ isWorkspaceTrusted: vi .fn() .mockReturnValue({ isTrusted: true, source: 'file' }), })); // NOW import everything else, including the (now effectively re-exported) settings.js import path, * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH import { describe, it, expect, vi, beforeEach, afterEach, type Mocked, type Mock, } from 'vitest'; import * as fs from 'node:fs'; // fs will be mocked separately import stripJsonComments from 'strip-json-comments'; // Will be mocked separately import { isWorkspaceTrusted } from './trustedFolders.js'; // These imports will get the versions from the vi.mock('./settings.js', ...) factory. import { getSettingsWarnings, loadSettings, USER_SETTINGS_PATH, // This IS the mocked path. getSystemSettingsPath, getSystemDefaultsPath, SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock. type Settings, loadEnvironment, SETTINGS_VERSION, SETTINGS_VERSION_KEY, } from './settings.js'; import { needsMigration } from './migration/index.js'; import { FatalConfigError, QWEN_DIR } from '@qwen-code/qwen-code-core'; const MOCK_WORKSPACE_DIR = '/mock/workspace'; // Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join( MOCK_WORKSPACE_DIR, SETTINGS_DIRECTORY_NAME, 'settings.json', ); // A more flexible type for test data that allows arbitrary properties. type TestSettings = Settings & { [key: string]: unknown; nested?: { [key: string]: unknown }; nestedObj?: { [key: string]: unknown }; }; vi.mock('node:fs', async (importOriginal) => { // Get all the functions from the real 'fs' module const actualFs = await importOriginal(); return { ...actualFs, // Keep all the real functions // Now, just override the ones we need for the test existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), renameSync: vi.fn(), mkdirSync: vi.fn(), realpathSync: (p: string) => p, }; }); // Also mock 'fs' for compatibility vi.mock('fs', async (importOriginal) => { // Get all the functions from the real 'fs' module const actualFs = await importOriginal(); return { ...actualFs, // Keep all the real functions // Now, just override the ones we need for the test existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), renameSync: vi.fn(), mkdirSync: vi.fn(), realpathSync: (p: string) => p, }; }); vi.mock('./extension.js', () => ({ disableExtension: vi.fn(), })); vi.mock('strip-json-comments', () => ({ default: vi.fn((content) => content), })); describe('Settings Loading and Merging', () => { let mockFsExistsSync: Mocked; let mockStripJsonComments: Mocked; let mockFsMkdirSync: Mocked; beforeEach(() => { vi.resetAllMocks(); mockFsExistsSync = vi.mocked(fs.existsSync); mockFsMkdirSync = vi.mocked(fs.mkdirSync); mockStripJsonComments = vi.mocked(stripJsonComments); vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user'); (mockStripJsonComments as unknown as Mock).mockImplementation( (jsonString: string) => jsonString, ); (mockFsExistsSync as Mock).mockReturnValue(false); (fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON (mockFsMkdirSync as Mock).mockImplementation(() => undefined); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); }); afterEach(() => { vi.restoreAllMocks(); }); describe('loadSettings', () => { it('should load empty settings if no files exist', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.system.settings).toEqual({}); expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({}); }); it('should load system settings if only system file exists', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === getSystemSettingsPath(), ); const systemSettingsContent = { ui: { theme: 'system-default', }, tools: { sandbox: false, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(fs.readFileSync).toHaveBeenCalledWith( getSystemSettingsPath(), 'utf-8', ); expect(settings.system.settings).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); }); it('should load user settings if only user file exists', () => { const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === expectedUserSettingsPath, ); const userSettingsContent = { ui: { theme: 'dark', }, context: { fileName: 'USER_CONTEXT.md', }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === expectedUserSettingsPath) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(fs.readFileSync).toHaveBeenCalledWith( expectedUserSettingsPath, 'utf-8', ); expect(settings.user.settings).toEqual({ ...userSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ ...userSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); }); it('should load workspace settings if only workspace file exists', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { tools: { sandbox: true, }, context: { fileName: 'WORKSPACE_CONTEXT.md', }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(fs.readFileSync).toHaveBeenCalledWith( MOCK_WORKSPACE_SETTINGS_PATH, 'utf-8', ); expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual({ ...workspaceSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.merged).toEqual({ ...workspaceSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); }); it('should merge system, user and workspace settings, with system taking precedence over workspace, and workspace over user', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === getSystemSettingsPath() || p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH, ); const systemSettingsContent = { ui: { theme: 'system-theme', }, tools: { sandbox: false, }, mcp: { allowed: ['server1', 'server2'], }, telemetry: { enabled: false }, }; const userSettingsContent = { ui: { theme: 'dark', }, tools: { sandbox: true, }, context: { fileName: 'USER_CONTEXT.md', }, }; const workspaceSettingsContent = { tools: { sandbox: false, core: ['tool1'], }, context: { fileName: 'WORKSPACE_CONTEXT.md', }, mcp: { allowed: ['server1', 'server2', 'server3'], }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.system.settings).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.user.settings).toEqual({ ...userSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.workspace.settings).toEqual({ ...workspaceSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.merged).toEqual({ [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, ui: { theme: 'system-theme', }, tools: { sandbox: false, core: ['tool1'], }, telemetry: { enabled: false }, context: { fileName: 'WORKSPACE_CONTEXT.md', }, mcp: { allowed: ['server1', 'server2'], }, }); }); it('should correctly migrate a complex legacy (v1) settings file', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const legacySettingsContent = { theme: 'legacy-dark', vimMode: true, contextFileName: 'LEGACY_CONTEXT.md', model: 'gemini-pro', mcpServers: { 'legacy-server-1': { command: 'npm', args: ['run', 'start:server1'], description: 'Legacy Server 1', }, 'legacy-server-2': { command: 'node', args: ['server2.js'], description: 'Legacy Server 2', }, }, allowMCPServers: ['legacy-server-1'], someUnrecognizedSetting: 'should-be-preserved', }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged).toEqual({ [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, ui: { theme: 'legacy-dark', }, general: { vimMode: true, }, context: { fileName: 'LEGACY_CONTEXT.md', }, model: { name: 'gemini-pro', }, mcpServers: { 'legacy-server-1': { command: 'npm', args: ['run', 'start:server1'], description: 'Legacy Server 1', }, 'legacy-server-2': { command: 'node', args: ['server2.js'], description: 'Legacy Server 2', }, }, mcp: { allowed: ['legacy-server-1'], }, someUnrecognizedSetting: 'should-be-preserved', }); }); it('should warn about ignored legacy keys in a v2 settings file', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, usageStatisticsEnabled: false, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(getSettingsWarnings(settings)).toEqual( expect.arrayContaining([ expect.stringContaining( "Legacy setting 'usageStatisticsEnabled' will be ignored", ), ]), ); expect(getSettingsWarnings(settings)).toEqual( expect.arrayContaining([ expect.stringContaining("'privacy.usageStatisticsEnabled'"), ]), ); }); it('should silently ignore unknown top-level keys in a v2 settings file', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, someUnknownKey: 'value', }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(getSettingsWarnings(settings)).toEqual([]); }); it('should not warn for valid v2 container keys', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, model: { name: 'qwen-coder' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(getSettingsWarnings(settings)).toEqual([]); }); it('should rewrite allowedTools to tools.allowed during migration', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const legacySettingsContent = { allowedTools: ['fs', 'shell'], }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.tools?.allowed).toEqual(['fs', 'shell']); expect((settings.merged as TestSettings)['allowedTools']).toBeUndefined(); }); it('should add version field to migrated settings file', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const legacySettingsContent = { theme: 'dark', model: 'qwen-coder', }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); // Verify that fs.writeFileSync was called with migrated settings including version expect(fs.writeFileSync).toHaveBeenCalled(); const writeCall = (fs.writeFileSync as Mock).mock.calls[0]; const writtenContent = JSON.parse(writeCall[1] as string); expect(writtenContent[SETTINGS_VERSION_KEY]).toBe(SETTINGS_VERSION); }); it('should not re-migrate settings that have version field', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const migratedSettingsContent = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, ui: { theme: 'dark', }, model: { name: 'qwen-coder', }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(migratedSettingsContent); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); // Verify that fs.renameSync and fs.writeFileSync were NOT called // (because no migration was needed) expect(fs.renameSync).not.toHaveBeenCalled(); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); it('should add version field to V2 settings without version and write to disk', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // V2 format but no version field const v2SettingsWithoutVersion = { ui: { theme: 'dark', }, model: { name: 'qwen-coder', }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(v2SettingsWithoutVersion); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); // Version normalization now uses writeWithBackupSync (temp write + rename) // Verify that writeFileSync was called with the temp file path const writeCall = (fs.writeFileSync as Mock).mock.calls.find( (call: unknown[]) => call[0] === `${USER_SETTINGS_PATH}.tmp`, ); expect(writeCall).toBeDefined(); if (!writeCall) { throw new Error('Expected temp write call for version normalization'); } const writtenContent = JSON.parse(writeCall[1] as string); expect(writtenContent[SETTINGS_VERSION_KEY]).toBe(SETTINGS_VERSION); expect(writtenContent.ui?.theme).toBe('dark'); expect(writtenContent.model?.name).toBe('qwen-coder'); // Verify writeWithBackupSync was called by checking temp file write expect(fs.writeFileSync).toHaveBeenCalled(); }); it('should correctly handle partially migrated settings without version field', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // Edge case: model already in V2 format (object), but autoAccept in V1 format const partiallyMigratedContent = { model: { name: 'qwen-coder', }, autoAccept: false, // V1 key }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(partiallyMigratedContent); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); // Verify that the migrated settings preserve the model object correctly expect(fs.writeFileSync).toHaveBeenCalled(); const writeCall = (fs.writeFileSync as Mock).mock.calls[0]; const writtenContent = JSON.parse(writeCall[1] as string); // Model should remain as an object, not double-nested expect(writtenContent.model).toEqual({ name: 'qwen-coder' }); // autoAccept should be migrated to tools.autoAccept expect(writtenContent.tools?.autoAccept).toBe(false); // Version field should be added expect(writtenContent[SETTINGS_VERSION_KEY]).toBe(SETTINGS_VERSION); }); it('should consolidate disableAutoUpdate and disableUpdateNag - both false means enableAutoUpdate is true', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // V1 settings with both disable* settings as false const legacySettingsContent = { disableAutoUpdate: false, disableUpdateNag: false, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); // Both are false, so enableAutoUpdate should be true expect(settings.merged.general?.enableAutoUpdate).toBe(true); }); it('should consolidate disableAutoUpdate and disableUpdateNag - any true means enableAutoUpdate is false', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // V1 settings with disableAutoUpdate=false but disableUpdateNag=true const legacySettingsContent = { disableAutoUpdate: false, disableUpdateNag: true, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); // disableUpdateNag is true, so enableAutoUpdate should be false expect(settings.merged.general?.enableAutoUpdate).toBe(false); }); it('should consolidate disableAutoUpdate and disableUpdateNag - disableAutoUpdate=true takes precedence', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // V1 settings with disableAutoUpdate=true const legacySettingsContent = { disableAutoUpdate: true, disableUpdateNag: false, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacySettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); // disableAutoUpdate is true, so enableAutoUpdate should be false expect(settings.merged.general?.enableAutoUpdate).toBe(false); }); it('should bump version to 3 even when V2 settings already have V3-compatible content', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); // V2 settings that already have V3-compatible keys (no migration needed) const v2SettingsWithV3Content = { $version: 2, general: { enableAutoUpdate: true, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(v2SettingsWithV3Content); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); // Version should be bumped to 3 even though no keys needed migration // writeWithBackupSync writes to a temp file first, then renames const writeCall = (fs.writeFileSync as Mock).mock.calls.find( (call: unknown[]) => call[0] === `${USER_SETTINGS_PATH}.tmp`, ); expect(writeCall).toBeDefined(); if (!writeCall) { throw new Error('Expected temp write call for V2->V3 version bump'); } const writtenContent = JSON.parse(writeCall[1] as string); expect(writtenContent.$version).toBe(SETTINGS_VERSION); }); it('should normalize invalid version metadata when no migration is applicable', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const invalidVersionSettings = { $version: 'invalid-version', general: { enableAutoUpdate: true, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(invalidVersionSettings); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); const writeCall = (fs.writeFileSync as Mock).mock.calls.find( (call: unknown[]) => call[0] === `${USER_SETTINGS_PATH}.tmp`, ); expect(writeCall).toBeDefined(); if (!writeCall) { throw new Error( 'Expected temp write call for invalid version normalization', ); } const writtenContent = JSON.parse(writeCall[1] as string); expect(writtenContent.$version).toBe(SETTINGS_VERSION); expect(writtenContent.general?.enableAutoUpdate).toBe(true); }); it('should normalize legacy numeric version when no migration can execute', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const staleVersionSettings = { $version: 1, // No V1/V2 indicators recognized by migrations customOnlyKey: 'value', }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(staleVersionSettings); return '{}'; }, ); loadSettings(MOCK_WORKSPACE_DIR); const writeCall = (fs.writeFileSync as Mock).mock.calls.find( (call: unknown[]) => call[0] === `${USER_SETTINGS_PATH}.tmp`, ); expect(writeCall).toBeDefined(); if (!writeCall) { throw new Error( 'Expected temp write call for stale version normalization', ); } const writtenContent = JSON.parse(writeCall[1] as string); expect(writtenContent.$version).toBe(SETTINGS_VERSION); expect(writtenContent.customOnlyKey).toBe('value'); }); it('should correctly merge and migrate legacy array properties from multiple scopes', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const legacyUserSettings = { includeDirectories: ['/user/dir'], excludeTools: ['user-tool'], excludedProjectEnvVars: ['USER_VAR'], }; const legacyWorkspaceSettings = { includeDirectories: ['/workspace/dir'], excludeTools: ['workspace-tool'], excludedProjectEnvVars: ['WORKSPACE_VAR', 'USER_VAR'], }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(legacyUserSettings); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(legacyWorkspaceSettings); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); // Verify includeDirectories are concatenated expect(settings.merged.context?.includeDirectories).toEqual([ '/user/dir', '/workspace/dir', ]); // Verify excludeTools are concatenated and de-duped expect(settings.merged.tools?.exclude).toEqual([ 'user-tool', 'workspace-tool', ]); // Verify excludedProjectEnvVars are concatenated and de-duped expect(settings.merged.advanced?.excludedEnvVars).toEqual( expect.arrayContaining(['USER_VAR', 'WORKSPACE_VAR']), ); expect(settings.merged.advanced?.excludedEnvVars).toHaveLength(2); }); it('should merge all settings files with the correct precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemDefaultsContent = { ui: { theme: 'default-theme', }, tools: { sandbox: true, }, telemetry: true, context: { includeDirectories: ['/system/defaults/dir'], }, }; const userSettingsContent = { ui: { theme: 'user-theme', }, context: { fileName: 'USER_CONTEXT.md', includeDirectories: ['/user/dir1', '/user/dir2'], }, }; const workspaceSettingsContent = { tools: { sandbox: false, }, context: { fileName: 'WORKSPACE_CONTEXT.md', includeDirectories: ['/workspace/dir'], }, }; const systemSettingsContent = { ui: { theme: 'system-theme', }, telemetry: false, context: { includeDirectories: ['/system/dir'], }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemDefaultsPath()) return JSON.stringify(systemDefaultsContent); if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.systemDefaults.settings).toEqual({ ...systemDefaultsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.system.settings).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.user.settings).toEqual({ ...userSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.workspace.settings).toEqual({ ...workspaceSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.merged).toEqual({ [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, context: { fileName: 'WORKSPACE_CONTEXT.md', includeDirectories: [ '/system/defaults/dir', '/user/dir1', '/user/dir2', '/workspace/dir', '/system/dir', ], }, telemetry: false, tools: { sandbox: false, }, ui: { theme: 'system-theme', }, }); }); it('should use folderTrust from workspace settings when trusted', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { security: { folderTrust: { enabled: true, }, }, }; const workspaceSettingsContent = { security: { folderTrust: { enabled: false, // This should be used }, }, }; const systemSettingsContent = { // No folderTrust here }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.security?.folderTrust?.enabled).toBe(false); // Workspace setting should be used }); it('should use system folderTrust over user setting', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { security: { folderTrust: { enabled: false, }, }, }; const workspaceSettingsContent = { security: { folderTrust: { enabled: true, // This should be ignored }, }, }; const systemSettingsContent = { security: { folderTrust: { enabled: true, }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used }); it('should handle contextFileName correctly when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { context: { fileName: 'CUSTOM.md' } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.context?.fileName).toBe('CUSTOM.md'); }); it('should handle contextFileName correctly when only in workspace settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { context: { fileName: 'PROJECT_SPECIFIC.md' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.context?.fileName).toBe('PROJECT_SPECIFIC.md'); }); it('should handle excludedProjectEnvVars correctly when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { general: {}, advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'CUSTOM_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'CUSTOM_VAR', ]); }); it('should handle excludedProjectEnvVars correctly when only in workspace settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { general: {}, advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); }); it('should merge excludedProjectEnvVars with workspace taking precedence over user', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH, ); const userSettingsContent = { general: {}, advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] }, }; const workspaceSettingsContent = { general: {}, advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', ]); expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); }); it('should default contextFileName to undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH, ); const userSettingsContent = { ui: { theme: 'dark' } }; const workspaceSettingsContent = { tools: { sandbox: true } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.context?.fileName).toBeUndefined(); }); it('should load telemetry setting from user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { telemetry: { enabled: true } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.telemetry?.enabled).toBe(true); }); it('should load telemetry setting from workspace settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { telemetry: { enabled: false } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.telemetry?.enabled).toBe(false); }); it('should prioritize workspace telemetry setting over user setting', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { telemetry: { enabled: true } }; const workspaceSettingsContent = { telemetry: { enabled: false } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.telemetry?.enabled).toBe(false); }); it('should have telemetry as undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist (fs.readFileSync as Mock).mockReturnValue('{}'); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.telemetry).toBeUndefined(); expect(settings.merged.ui).toBeUndefined(); expect(settings.merged.mcpServers).toBeUndefined(); }); it('should merge MCP servers correctly, with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH, ); const userSettingsContent = { mcpServers: { 'user-server': { command: 'user-command', args: ['--user-arg'], description: 'User MCP server', }, 'shared-server': { command: 'user-shared-command', description: 'User shared server config', }, }, }; const workspaceSettingsContent = { mcpServers: { 'workspace-server': { command: 'workspace-command', args: ['--workspace-arg'], description: 'Workspace MCP server', }, 'shared-server': { command: 'workspace-shared-command', description: 'Workspace shared server config', }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.user.settings).toEqual({ ...userSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.workspace.settings).toEqual({ ...workspaceSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.merged.mcpServers).toEqual({ 'user-server': { command: 'user-command', args: ['--user-arg'], description: 'User MCP server', }, 'workspace-server': { command: 'workspace-command', args: ['--workspace-arg'], description: 'Workspace MCP server', }, 'shared-server': { command: 'workspace-shared-command', description: 'Workspace shared server config', }, }); }); it('should handle MCP servers when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { mcpServers: { 'user-only-server': { command: 'user-only-command', description: 'User only server', }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.mcpServers).toEqual({ 'user-only-server': { command: 'user-only-command', description: 'User only server', }, }); }); it('should handle MCP servers when only in workspace settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { mcpServers: { 'workspace-only-server': { command: 'workspace-only-command', description: 'Workspace only server', }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return ''; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.mcpServers).toEqual({ 'workspace-only-server': { command: 'workspace-only-command', description: 'Workspace only server', }, }); }); it('should have mcpServers as undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist (fs.readFileSync as Mock).mockReturnValue('{}'); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.mcpServers).toBeUndefined(); }); it('should merge MCP servers from system, user, and workspace with system taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemSettingsContent = { mcpServers: { 'shared-server': { command: 'system-command', args: ['--system-arg'], }, 'system-only-server': { command: 'system-only-command', }, }, }; const userSettingsContent = { mcpServers: { 'user-server': { command: 'user-command', }, 'shared-server': { command: 'user-command', description: 'from user', }, }, }; const workspaceSettingsContent = { mcpServers: { 'workspace-server': { command: 'workspace-command', }, 'shared-server': { command: 'workspace-command', args: ['--workspace-arg'], }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.mcpServers).toEqual({ 'user-server': { command: 'user-command', }, 'workspace-server': { command: 'workspace-command', }, 'system-only-server': { command: 'system-only-command', }, 'shared-server': { command: 'system-command', args: ['--system-arg'], }, }); }); it('should merge mcp allowed/excluded lists with system taking precedence over workspace', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemSettingsContent = { mcp: { allowed: ['system-allowed'], }, }; const userSettingsContent = { mcp: { allowed: ['user-allowed'], excluded: ['user-excluded'], }, }; const workspaceSettingsContent = { mcp: { allowed: ['workspace-allowed'], excluded: ['workspace-excluded'], }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.mcp).toEqual({ allowed: ['system-allowed'], excluded: ['workspace-excluded'], }); }); it('should merge chatCompression settings, with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { general: {}, model: { chatCompression: { contextPercentageThreshold: 0.5 } }, }; const workspaceSettingsContent = { general: {}, model: { chatCompression: { contextPercentageThreshold: 0.8 } }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); const e = settings.user.settings.model?.chatCompression; console.log(e); expect(settings.user.settings.model?.chatCompression).toEqual({ contextPercentageThreshold: 0.5, }); expect(settings.workspace.settings.model?.chatCompression).toEqual({ contextPercentageThreshold: 0.8, }); expect(settings.merged.model?.chatCompression).toEqual({ contextPercentageThreshold: 0.8, }); }); it('should merge output format settings, with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { output: { format: 'text' }, }; const workspaceSettingsContent = { output: { format: 'json' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.output?.format).toBe('json'); }); it('should handle chatCompression when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { general: {}, model: { chatCompression: { contextPercentageThreshold: 0.5 } }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.model?.chatCompression).toEqual({ contextPercentageThreshold: 0.5, }); }); it('should have model as undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist (fs.readFileSync as Mock).mockReturnValue('{}'); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.model).toBeUndefined(); }); it('should ignore chatCompression if contextPercentageThreshold is invalid', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { general: {}, model: { chatCompression: { contextPercentageThreshold: 1.5 } }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.model?.chatCompression).toEqual({ contextPercentageThreshold: 1.5, }); warnSpy.mockRestore(); }); it('should deep merge chatCompression settings', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { general: {}, model: { chatCompression: { contextPercentageThreshold: 0.5 } }, }; const workspaceSettingsContent = { general: {}, model: { chatCompression: {} }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.model?.chatCompression).toEqual({ contextPercentageThreshold: 0.5, }); }); it('should merge includeDirectories from all scopes', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemSettingsContent = { context: { includeDirectories: ['/system/dir'] }, }; const systemDefaultsContent = { context: { includeDirectories: ['/system/defaults/dir'] }, }; const userSettingsContent = { context: { includeDirectories: ['/user/dir1', '/user/dir2'] }, }; const workspaceSettingsContent = { context: { includeDirectories: ['/workspace/dir'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === getSystemDefaultsPath()) return JSON.stringify(systemDefaultsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.context?.includeDirectories).toEqual([ '/system/defaults/dir', '/user/dir1', '/user/dir2', '/workspace/dir', '/system/dir', ]); }); it('should handle JSON parsing errors gracefully', () => { (mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist" const invalidJsonContent = 'invalid json'; const userReadError = new SyntaxError( "Expected ',' or '}' after property value in JSON at position 10", ); const workspaceReadError = new SyntaxError( 'Unexpected token i in JSON at position 0', ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) { // Simulate JSON.parse throwing for user settings vi.spyOn(JSON, 'parse').mockImplementationOnce(() => { throw userReadError; }); return invalidJsonContent; // Content that would cause JSON.parse to throw } if (p === MOCK_WORKSPACE_SETTINGS_PATH) { // Simulate JSON.parse throwing for workspace settings vi.spyOn(JSON, 'parse').mockImplementationOnce(() => { throw workspaceReadError; }); return invalidJsonContent; } return '{}'; // Default for other reads }, ); try { loadSettings(MOCK_WORKSPACE_DIR); throw new Error('loadSettings should have thrown a FatalConfigError'); } catch (e) { expect(e).toBeInstanceOf(FatalConfigError); const error = e as FatalConfigError; expect(error.message).toContain( `Error in ${USER_SETTINGS_PATH}: ${userReadError.message}`, ); expect(error.message).toContain( `Error in ${MOCK_WORKSPACE_SETTINGS_PATH}: ${workspaceReadError.message}`, ); expect(error.message).toContain( 'Please fix the configuration file(s) and try again.', ); } // Restore JSON.parse mock if it was spied on specifically for this test vi.restoreAllMocks(); // Or more targeted restore if needed }); it('should resolve environment variables in user settings', () => { process.env['TEST_API_KEY'] = 'user_api_key_from_env'; const userSettingsContent: TestSettings = { apiKey: '$TEST_API_KEY', someUrl: 'https://test.com/${TEST_API_KEY}', }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['apiKey']).toBe( 'user_api_key_from_env', ); expect((settings.user.settings as TestSettings)['someUrl']).toBe( 'https://test.com/user_api_key_from_env', ); expect((settings.merged as TestSettings)['apiKey']).toBe( 'user_api_key_from_env', ); delete process.env['TEST_API_KEY']; }); it('should resolve environment variables in workspace settings', () => { process.env['WORKSPACE_ENDPOINT'] = 'workspace_endpoint_from_env'; const workspaceSettingsContent: TestSettings = { endpoint: '${WORKSPACE_ENDPOINT}/api', nested: { value: '$WORKSPACE_ENDPOINT' }, }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.workspace.settings as TestSettings)['endpoint']).toBe( 'workspace_endpoint_from_env/api', ); expect( ( (settings.workspace.settings as TestSettings).nested as { [key: string]: unknown; } )['value'], ).toBe('workspace_endpoint_from_env'); expect((settings.merged as TestSettings)['endpoint']).toBe( 'workspace_endpoint_from_env/api', ); delete process.env['WORKSPACE_ENDPOINT']; }); it('should correctly resolve and merge env variables from different scopes', () => { process.env['SYSTEM_VAR'] = 'system_value'; process.env['USER_VAR'] = 'user_value'; process.env['WORKSPACE_VAR'] = 'workspace_value'; process.env['SHARED_VAR'] = 'final_value'; const systemSettingsContent: TestSettings = { configValue: '$SHARED_VAR', systemOnly: '$SYSTEM_VAR', }; const userSettingsContent: TestSettings = { configValue: '$SHARED_VAR', userOnly: '$USER_VAR', ui: { theme: 'dark', }, }; const workspaceSettingsContent: TestSettings = { configValue: '$SHARED_VAR', workspaceOnly: '$WORKSPACE_VAR', ui: { theme: 'light', }, }; (mockFsExistsSync as Mock).mockReturnValue(true); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === getSystemSettingsPath()) { return JSON.stringify(systemSettingsContent); } if (p === USER_SETTINGS_PATH) { return JSON.stringify(userSettingsContent); } if (p === MOCK_WORKSPACE_SETTINGS_PATH) { return JSON.stringify(workspaceSettingsContent); } return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); // Check resolved values in individual scopes expect((settings.system.settings as TestSettings)['configValue']).toBe( 'final_value', ); expect((settings.system.settings as TestSettings)['systemOnly']).toBe( 'system_value', ); expect((settings.user.settings as TestSettings)['configValue']).toBe( 'final_value', ); expect((settings.user.settings as TestSettings)['userOnly']).toBe( 'user_value', ); expect((settings.workspace.settings as TestSettings)['configValue']).toBe( 'final_value', ); expect( (settings.workspace.settings as TestSettings)['workspaceOnly'], ).toBe('workspace_value'); // Check merged values (system > workspace > user) expect((settings.merged as TestSettings)['configValue']).toBe( 'final_value', ); expect((settings.merged as TestSettings)['systemOnly']).toBe( 'system_value', ); expect((settings.merged as TestSettings)['userOnly']).toBe('user_value'); expect((settings.merged as TestSettings)['workspaceOnly']).toBe( 'workspace_value', ); expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user delete process.env['SYSTEM_VAR']; delete process.env['USER_VAR']; delete process.env['WORKSPACE_VAR']; delete process.env['SHARED_VAR']; }); it('should correctly merge dnsResolutionOrder with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { advanced: { dnsResolutionOrder: 'ipv4first' }, }; const workspaceSettingsContent = { advanced: { dnsResolutionOrder: 'verbatim' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim'); }); it('should use user dnsResolutionOrder if workspace is not defined', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { advanced: { dnsResolutionOrder: 'verbatim' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim'); }); it('should leave unresolved environment variables as is', () => { const userSettingsContent: TestSettings = { apiKey: '$UNDEFINED_VAR' }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['apiKey']).toBe( '$UNDEFINED_VAR', ); expect((settings.merged as TestSettings)['apiKey']).toBe( '$UNDEFINED_VAR', ); }); it('should resolve multiple environment variables in a single string', () => { process.env['VAR_A'] = 'valueA'; process.env['VAR_B'] = 'valueB'; const userSettingsContent: TestSettings = { path: '/path/$VAR_A/${VAR_B}/end', }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['path']).toBe( '/path/valueA/valueB/end', ); delete process.env['VAR_A']; delete process.env['VAR_B']; }); it('should resolve environment variables in arrays', () => { process.env['ITEM_1'] = 'item1_env'; process.env['ITEM_2'] = 'item2_env'; const userSettingsContent: TestSettings = { list: ['$ITEM_1', '${ITEM_2}', 'literal'], }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['list']).toEqual([ 'item1_env', 'item2_env', 'literal', ]); delete process.env['ITEM_1']; delete process.env['ITEM_2']; }); it('should correctly pass through null, boolean, and number types, and handle undefined properties', () => { process.env['MY_ENV_STRING'] = 'env_string_value'; process.env['MY_ENV_STRING_NESTED'] = 'env_string_nested_value'; const userSettingsContent: TestSettings = { nullVal: null, trueVal: true, falseVal: false, numberVal: 123.45, stringVal: '$MY_ENV_STRING', nestedObj: { nestedNull: null, nestedBool: true, nestedNum: 0, nestedString: 'literal', anotherEnv: '${MY_ENV_STRING_NESTED}', }, }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['nullVal']).toBeNull(); expect((settings.user.settings as TestSettings)['trueVal']).toBe(true); expect((settings.user.settings as TestSettings)['falseVal']).toBe(false); expect((settings.user.settings as TestSettings)['numberVal']).toBe( 123.45, ); expect((settings.user.settings as TestSettings)['stringVal']).toBe( 'env_string_value', ); expect( (settings.user.settings as TestSettings)['undefinedVal'], ).toBeUndefined(); expect( ( (settings.user.settings as TestSettings).nestedObj as { [key: string]: unknown; } )['nestedNull'], ).toBeNull(); expect( ( (settings.user.settings as TestSettings).nestedObj as { [key: string]: unknown; } )['nestedBool'], ).toBe(true); expect( ( (settings.user.settings as TestSettings).nestedObj as { [key: string]: unknown; } )['nestedNum'], ).toBe(0); expect( ( (settings.user.settings as TestSettings).nestedObj as { [key: string]: unknown; } )['nestedString'], ).toBe('literal'); expect( ( (settings.user.settings as TestSettings).nestedObj as { [key: string]: unknown; } )['anotherEnv'], ).toBe('env_string_nested_value'); delete process.env['MY_ENV_STRING']; delete process.env['MY_ENV_STRING_NESTED']; }); it('should resolve multiple concatenated environment variables in a single string value', () => { process.env['TEST_HOST'] = 'myhost'; process.env['TEST_PORT'] = '9090'; const userSettingsContent: TestSettings = { serverAddress: '${TEST_HOST}:${TEST_PORT}/api', }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect((settings.user.settings as TestSettings)['serverAddress']).toBe( 'myhost:9090/api', ); delete process.env['TEST_HOST']; delete process.env['TEST_PORT']; }); describe('when QWEN_CODE_SYSTEM_SETTINGS_PATH is set', () => { const MOCK_ENV_SYSTEM_SETTINGS_PATH = '/mock/env/system/settings.json'; beforeEach(() => { process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = MOCK_ENV_SYSTEM_SETTINGS_PATH; }); afterEach(() => { delete process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']; }); it('should load system settings from the path specified in the environment variable', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_ENV_SYSTEM_SETTINGS_PATH, ); const systemSettingsContent = { ui: { theme: 'env-var-theme' }, tools: { sandbox: true }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_ENV_SYSTEM_SETTINGS_PATH) return JSON.stringify(systemSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(fs.readFileSync).toHaveBeenCalledWith( MOCK_ENV_SYSTEM_SETTINGS_PATH, 'utf-8', ); expect(settings.system.path).toBe(MOCK_ENV_SYSTEM_SETTINGS_PATH); expect(settings.system.settings).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); expect(settings.merged).toEqual({ ...systemSettingsContent, [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, }); }); }); }); describe('excludedProjectEnvVars integration', () => { const originalEnv = { ...process.env }; beforeEach(() => { process.env = { ...originalEnv }; }); afterEach(() => { process.env = originalEnv; }); it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => { // Create a workspace settings file with excludedProjectEnvVars const workspaceSettingsContent = { general: {}, advanced: { excludedEnvVars: ['DEBUG', 'DEBUG_MODE'] }, }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); // Mock findEnvFile to return a project .env file const originalFindEnvFile = ( loadSettings as unknown as { findEnvFile: () => string } ).findEnvFile; (loadSettings as unknown as { findEnvFile: () => string }).findEnvFile = () => '/mock/project/.env'; // Mock fs.readFileSync for .env file content const originalReadFileSync = fs.readFileSync; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === '/mock/project/.env') { return 'DEBUG=true\nDEBUG_MODE=1\nGEMINI_API_KEY=test-key'; } if (p === MOCK_WORKSPACE_SETTINGS_PATH) { return JSON.stringify(workspaceSettingsContent); } return '{}'; }, ); try { // This will call loadEnvironment internally with the merged settings const settings = loadSettings(MOCK_WORKSPACE_DIR); // Verify the settings were loaded correctly expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'DEBUG_MODE', ]); // Note: We can't directly test process.env changes here because the mocking // prevents the actual file system operations, but we can verify the settings // are correctly merged and passed to loadEnvironment } finally { (loadSettings as unknown as { findEnvFile: () => string }).findEnvFile = originalFindEnvFile; (fs.readFileSync as Mock).mockImplementation(originalReadFileSync); } }); it('should respect custom excludedProjectEnvVars from user settings', () => { const userSettingsContent = { general: {}, advanced: { excludedEnvVars: ['NODE_ENV', 'DEBUG'] }, }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'NODE_ENV', 'DEBUG', ]); expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'NODE_ENV', 'DEBUG', ]); }); it('should merge excludedProjectEnvVars with workspace taking precedence', () => { const userSettingsContent = { general: {}, advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] }, }; const workspaceSettingsContent = { general: {}, advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (mockFsExistsSync as Mock).mockReturnValue(true); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', ]); expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); }); }); describe('with workspace trust', () => { it('should merge workspace settings when workspace is trusted', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { ui: { theme: 'dark' }, tools: { sandbox: false }, }; const workspaceSettingsContent = { tools: { sandbox: true }, context: { fileName: 'WORKSPACE.md' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.tools?.sandbox).toBe(true); expect(settings.merged.context?.fileName).toBe('WORKSPACE.md'); expect(settings.merged.ui?.theme).toBe('dark'); }); it('should NOT merge workspace settings when workspace is not trusted', () => { vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: false, source: 'file', }); (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { ui: { theme: 'dark' }, tools: { sandbox: false }, context: { fileName: 'USER.md' }, }; const workspaceSettingsContent = { tools: { sandbox: true }, context: { fileName: 'WORKSPACE.md' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(settings.merged.tools?.sandbox).toBe(false); // User setting expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting expect(settings.merged.ui?.theme).toBe('dark'); // User setting }); }); describe('loadEnvironment', () => { function setup({ isFolderTrustEnabled = true, isWorkspaceTrustedValue = true, }) { delete process.env['TESTTEST']; // reset const geminiEnvPath = path.resolve(path.join(QWEN_DIR, '.env')); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: isWorkspaceTrustedValue, source: 'file', }); (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()), ); const userSettingsContent: Settings = { ui: { theme: 'dark', }, security: { folderTrust: { enabled: isFolderTrustEnabled, }, }, context: { fileName: 'USER_CONTEXT.md', }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === geminiEnvPath) return 'TESTTEST=1234'; return '{}'; }, ); } it('sets environment variables from .env files', () => { setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true }); loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged); expect(process.env['TESTTEST']).toEqual('1234'); }); it('does not load project .env files from untrusted workspaces', () => { delete process.env['PROJECT_ENV_VAR']; const cwdSpy = vi .spyOn(process, 'cwd') .mockReturnValue(MOCK_WORKSPACE_DIR); const projectEnvPath = path.join(MOCK_WORKSPACE_DIR, '.env'); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: false, source: 'file', }); (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, projectEnvPath].includes(p.toString()), ); const userSettingsContent: Settings = { ui: { theme: 'dark', }, security: { folderTrust: { enabled: true, }, }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === projectEnvPath) return 'PROJECT_ENV_VAR=from_project'; return '{}'; }, ); loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged); // Project .env should NOT be loaded when workspace is untrusted expect(process.env['PROJECT_ENV_VAR']).toBeUndefined(); cwdSpy.mockRestore(); }); describe('settings.env field', () => { const originalEnv = { ...process.env }; beforeEach(() => { process.env = { ...originalEnv }; delete process.env['ENV_FROM_SETTINGS']; delete process.env['ENV_OVERRIDE_TEST']; delete process.env['SYSTEM_ENV_VAR']; delete process.env['MULTI_VAR_A']; delete process.env['MULTI_VAR_B']; delete process.env['MULTI_VAR_C']; delete process.env['USER_ENV_VAR']; delete process.env['WORKSPACE_ENV_VAR']; }); afterEach(() => { process.env = originalEnv; }); it('should load environment variables from settings.env as fallback', () => { const userSettingsContent: Settings = { env: { ENV_FROM_SETTINGS: 'settings_value', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH].includes(p.toString()), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); // loadSettings internally calls loadEnvironment with userSettings loadSettings(MOCK_WORKSPACE_DIR); expect(process.env['ENV_FROM_SETTINGS']).toEqual('settings_value'); }); it('should allow .env file to override settings.env values', () => { const geminiEnvPath = path.resolve(path.join(QWEN_DIR, '.env')); const userSettingsContent: Settings = { env: { ENV_OVERRIDE_TEST: 'from_settings', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === geminiEnvPath) return 'ENV_OVERRIDE_TEST=from_dotenv'; return '{}'; }, ); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); // loadSettings internally calls loadEnvironment with merged settings loadSettings(MOCK_WORKSPACE_DIR); // .env file has higher priority than settings.env (loaded first, no-override) expect(process.env['ENV_OVERRIDE_TEST']).toEqual('from_dotenv'); }); it('should not override existing system environment variables', () => { process.env['SYSTEM_ENV_VAR'] = 'system_value'; const geminiEnvPath = path.resolve(path.join(QWEN_DIR, '.env')); const userSettingsContent: Settings = { env: { SYSTEM_ENV_VAR: 'from_settings', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === geminiEnvPath) return 'SYSTEM_ENV_VAR=from_dotenv'; return '{}'; }, ); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); // loadSettings internally calls loadEnvironment with userSettings loadSettings(MOCK_WORKSPACE_DIR); // System environment variable should have highest priority expect(process.env['SYSTEM_ENV_VAR']).toEqual('system_value'); }); it('should support multiple env variables in settings.env', () => { const userSettingsContent: Settings = { env: { MULTI_VAR_A: 'value_a', MULTI_VAR_B: 'value_b', MULTI_VAR_C: 'value_c', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH].includes(p.toString()), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); return '{}'; }, ); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); // loadSettings internally calls loadEnvironment with userSettings loadSettings(MOCK_WORKSPACE_DIR); expect(process.env['MULTI_VAR_A']).toEqual('value_a'); expect(process.env['MULTI_VAR_B']).toEqual('value_b'); expect(process.env['MULTI_VAR_C']).toEqual('value_c'); }); it('should load settings.env from both user and workspace settings', () => { const workspaceSettingsContent = { env: { WORKSPACE_ENV_VAR: 'workspace_value', }, }; const userSettingsContent: Settings = { env: { USER_ENV_VAR: 'user_value', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, MOCK_WORKSPACE_SETTINGS_PATH].includes( p.toString(), ), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', }); // loadSettings internally calls loadEnvironment with merged settings loadSettings(MOCK_WORKSPACE_DIR); // Both user-level and workspace-level env should be loaded expect(process.env['USER_ENV_VAR']).toEqual('user_value'); expect(process.env['WORKSPACE_ENV_VAR']).toEqual('workspace_value'); }); it('should load user-level settings.env even when workspace is untrusted', () => { const userSettingsContent: Settings = { env: { USER_ENV_VAR: 'user_value', }, }; const workspaceSettingsContent = { env: { WORKSPACE_ENV_VAR: 'workspace_value', }, }; (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => [USER_SETTINGS_PATH, MOCK_WORKSPACE_SETTINGS_PATH].includes( p.toString(), ), ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); if (p === MOCK_WORKSPACE_SETTINGS_PATH) return JSON.stringify(workspaceSettingsContent); return '{}'; }, ); // Workspace is untrusted vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: false, source: 'file', }); loadSettings(MOCK_WORKSPACE_DIR); // User-level settings.env should still be loaded even when untrusted expect(process.env['USER_ENV_VAR']).toEqual('user_value'); // Workspace-level settings.env should NOT be loaded (filtered by mergeSettings) expect(process.env['WORKSPACE_ENV_VAR']).toBeUndefined(); }); }); }); describe('needsMigration', () => { it('should return false for an empty object', () => { expect(needsMigration({})).toBe(false); }); it('should return false for settings that are already in V2 format', () => { const v2Settings: Partial = { ui: { theme: 'dark', }, tools: { sandbox: true, }, }; expect(needsMigration(v2Settings)).toBe(false); }); it('should return true for settings with a V1 key that needs to be moved', () => { const v1Settings = { theme: 'dark', // v1 key }; expect(needsMigration(v1Settings)).toBe(true); }); it('should return true for settings with a mix of V1 and V2 keys', () => { const mixedSettings = { theme: 'dark', // v1 key tools: { sandbox: true, // v2 key }, }; expect(needsMigration(mixedSettings)).toBe(true); }); it('should return false for settings with only V1 keys that are the same in V2', () => { const v1Settings = { mcpServers: {}, telemetry: {}, extensions: [], }; expect(needsMigration(v1Settings)).toBe(false); }); it('should return true for settings with a mix of V1 keys that are the same in V2 and V1 keys that need moving', () => { const v1Settings = { mcpServers: {}, // same in v2 theme: 'dark', // needs moving }; expect(needsMigration(v1Settings)).toBe(true); }); it('should return false for settings with unrecognized keys', () => { const settings = { someUnrecognizedKey: 'value', }; expect(needsMigration(settings)).toBe(false); }); it('should return false for settings with v2 keys and unrecognized keys', () => { const settings = { ui: { theme: 'dark' }, someUnrecognizedKey: 'value', }; expect(needsMigration(settings)).toBe(false); }); describe('with version field', () => { it('should return false when version field indicates current or newer version', () => { const settingsWithVersion = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, theme: 'dark', // Even though this is a V1 key, version field takes precedence }; expect(needsMigration(settingsWithVersion)).toBe(false); }); it('should return false when version field indicates a newer version', () => { const settingsWithNewerVersion = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION + 1, theme: 'dark', }; expect(needsMigration(settingsWithNewerVersion)).toBe(false); }); it('should return true when version field indicates an older version', () => { const settingsWithOldVersion = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION - 1, theme: 'dark', }; expect(needsMigration(settingsWithOldVersion)).toBe(true); }); it('should use fallback logic when version field is not a number', () => { const settingsWithInvalidVersion = { [SETTINGS_VERSION_KEY]: 'not-a-number', theme: 'dark', }; expect(needsMigration(settingsWithInvalidVersion)).toBe(true); }); it('should use fallback logic when version field is missing', () => { const settingsWithoutVersion = { theme: 'dark', }; expect(needsMigration(settingsWithoutVersion)).toBe(true); }); }); describe('edge case: partially migrated settings', () => { it('should return true for partially migrated settings without version field', () => { // This simulates the dangerous edge case: model already in V2 format, // but other fields in V1 format const partiallyMigrated = { model: { name: 'qwen-coder', }, autoAccept: false, // V1 key }; expect(needsMigration(partiallyMigrated)).toBe(true); }); it('should return false for partially migrated settings WITH version field', () => { // With version field, we trust that it's been properly migrated const partiallyMigratedWithVersion = { [SETTINGS_VERSION_KEY]: SETTINGS_VERSION, model: { name: 'qwen-coder', }, autoAccept: false, // This would look like V1 but version says it's V2 }; expect(needsMigration(partiallyMigratedWithVersion)).toBe(false); }); }); }); });