mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-14 10:24:30 +00:00
fix(config): prevent profile loss from strict config validation
Relaxed isUnifiedConfig() type guard to accept version >= 1 and partial configs. Added mergeWithDefaults() to preserve user data while filling missing sections. Fixes profile "not found" after terminal restart. Closes #82
This commit is contained in:
@@ -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>): 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.
|
* Load config, preferring YAML if available, falling back to creating empty config.
|
||||||
|
* Merges with defaults to ensure all sections exist.
|
||||||
*/
|
*/
|
||||||
export function loadOrCreateUnifiedConfig(): UnifiedConfig {
|
export function loadOrCreateUnifiedConfig(): UnifiedConfig {
|
||||||
const existing = loadUnifiedConfig();
|
const existing = loadUnifiedConfig();
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
// Merge with defaults to fill any missing sections
|
||||||
|
return mergeWithDefaults(existing);
|
||||||
|
}
|
||||||
|
|
||||||
// Create empty config
|
// Create empty config
|
||||||
const config = createEmptyUnifiedConfig();
|
const config = createEmptyUnifiedConfig();
|
||||||
|
|||||||
@@ -151,17 +151,15 @@ export function createEmptySecretsConfig(): SecretsConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard for UnifiedConfig.
|
* 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 {
|
export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig {
|
||||||
if (typeof obj !== 'object' || obj === null) return false;
|
if (typeof obj !== 'object' || obj === null) return false;
|
||||||
const config = obj as Record<string, unknown>;
|
const config = obj as Record<string, unknown>;
|
||||||
return (
|
// Only require version to be a number >= 1 (allow future versions)
|
||||||
typeof config.version === 'number' &&
|
// Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig
|
||||||
config.version === UNIFIED_CONFIG_VERSION &&
|
return typeof config.version === 'number' && config.version >= 1;
|
||||||
typeof config.accounts === 'object' &&
|
|
||||||
typeof config.profiles === 'object' &&
|
|
||||||
typeof config.cliproxy === 'object'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -124,14 +124,23 @@ describe('unified-config-types', () => {
|
|||||||
expect(isUnifiedConfig(null)).toBe(false);
|
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 };
|
const config = { ...createEmptyUnifiedConfig(), version: 1 };
|
||||||
expect(isUnifiedConfig(config)).toBe(false);
|
expect(isUnifiedConfig(config)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for missing fields', () => {
|
it('should return true for partial configs (relaxed validation)', () => {
|
||||||
expect(isUnifiedConfig({ version: 2 })).toBe(false);
|
// Fix for issue #82: Relaxed validation accepts partial configs
|
||||||
expect(isUnifiedConfig({ version: 2, accounts: {} })).toBe(false);
|
// 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user