diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 505928f3..292ec9e5 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -90,12 +90,39 @@ export function loadUnifiedConfig(): UnifiedConfig | null { } } +/** + * Merge partial config with defaults. + * Preserves existing data while filling in missing sections. + */ +function mergeWithDefaults(partial: Partial): UnifiedConfig { + const defaults = createEmptyUnifiedConfig(); + return { + version: partial.version ?? defaults.version, + default: partial.default ?? defaults.default, + accounts: partial.accounts ?? defaults.accounts, + profiles: partial.profiles ?? defaults.profiles, + cliproxy: { + oauth_accounts: partial.cliproxy?.oauth_accounts ?? defaults.cliproxy.oauth_accounts, + providers: defaults.cliproxy.providers, // Always use defaults for providers + variants: partial.cliproxy?.variants ?? defaults.cliproxy.variants, + }, + preferences: { + ...defaults.preferences, + ...partial.preferences, + }, + }; +} + /** * Load config, preferring YAML if available, falling back to creating empty config. + * Merges with defaults to ensure all sections exist. */ export function loadOrCreateUnifiedConfig(): UnifiedConfig { const existing = loadUnifiedConfig(); - if (existing) return existing; + if (existing) { + // Merge with defaults to fill any missing sections + return mergeWithDefaults(existing); + } // Create empty config const config = createEmptyUnifiedConfig(); diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index e41cc4a1..ed313298 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -151,17 +151,15 @@ export function createEmptySecretsConfig(): SecretsConfig { /** * Type guard for UnifiedConfig. + * Relaxed validation: accepts configs with version >= 1 and any subset of sections. + * Missing sections will be filled with defaults during merge. */ export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { if (typeof obj !== 'object' || obj === null) return false; const config = obj as Record; - return ( - typeof config.version === 'number' && - config.version === UNIFIED_CONFIG_VERSION && - typeof config.accounts === 'object' && - typeof config.profiles === 'object' && - typeof config.cliproxy === 'object' - ); + // Only require version to be a number >= 1 (allow future versions) + // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig + return typeof config.version === 'number' && config.version >= 1; } /** diff --git a/tests/unit/unified-config.test.ts b/tests/unit/unified-config.test.ts index 3ad80b22..b345bd34 100644 --- a/tests/unit/unified-config.test.ts +++ b/tests/unit/unified-config.test.ts @@ -124,14 +124,23 @@ describe('unified-config-types', () => { expect(isUnifiedConfig(null)).toBe(false); }); - it('should return false for wrong version', () => { + it('should return true for older version (relaxed validation)', () => { + // Fix for issue #82: Relaxed validation accepts version >= 1 + // to prevent profile loss when loading partially valid configs const config = { ...createEmptyUnifiedConfig(), version: 1 }; - expect(isUnifiedConfig(config)).toBe(false); + expect(isUnifiedConfig(config)).toBe(true); }); - it('should return false for missing fields', () => { - expect(isUnifiedConfig({ version: 2 })).toBe(false); - expect(isUnifiedConfig({ version: 2, accounts: {} })).toBe(false); + it('should return true for partial configs (relaxed validation)', () => { + // Fix for issue #82: Relaxed validation accepts partial configs + // Missing sections are merged with defaults in loadOrCreateUnifiedConfig + expect(isUnifiedConfig({ version: 2 })).toBe(true); + expect(isUnifiedConfig({ version: 2, accounts: {} })).toBe(true); + }); + + it('should return false for version < 1', () => { + expect(isUnifiedConfig({ version: 0 })).toBe(false); + expect(isUnifiedConfig({ version: -1 })).toBe(false); }); });