mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-20 05:11:48 +00:00
Merge branch 'kai/refactor/1135-item2-config-loader-facade' into kai/refactor/1135-structural-maintainability
This commit is contained in:
commit
cb8b34b36d
2 files changed
+311
No files matched your search
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Config Loader Facade Unit Tests
|
||||||
|
*
|
||||||
|
* Tests memoization cache behavior, cache invalidation on write ops,
|
||||||
|
* and verifies all re-exports are present from underlying modules.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: create a temp home dir with a minimal valid config.yaml so
|
||||||
|
* loadOrCreateUnifiedConfig succeeds without touching the real ~/.ccs.
|
||||||
|
*/
|
||||||
|
function createTestHome(): string {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-facade-test-'));
|
||||||
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
const configPath = path.join(ccsDir, 'config.yaml');
|
||||||
|
fs.writeFileSync(configPath, `version: 1\n`, 'utf8');
|
||||||
|
return tempHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: get the facade module, bypassing the import cache each time.
|
||||||
|
* We use dynamic import with a cache-busting query param so beforeEach
|
||||||
|
* re-imports get a fresh module with a clean cache state.
|
||||||
|
*/
|
||||||
|
async function importFacade(): Promise<typeof import('../config-loader-facade')> {
|
||||||
|
return import(`../config-loader-facade?cachebust=${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('config-loader-facade', () => {
|
||||||
|
let tempHome: string;
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempHome = createTestHome();
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore env
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp dir
|
||||||
|
if (tempHome && fs.existsSync(tempHome)) {
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('re-exports from unified-config-loader', () => {
|
||||||
|
it('should export all core loader functions', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
expect(typeof facade.loadUnifiedConfig).toBe('function');
|
||||||
|
expect(typeof facade.loadOrCreateUnifiedConfig).toBe('function');
|
||||||
|
expect(typeof facade.saveUnifiedConfig).toBe('function');
|
||||||
|
expect(typeof facade.mutateUnifiedConfig).toBe('function');
|
||||||
|
expect(typeof facade.updateUnifiedConfig).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export all path/format utilities', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
expect(typeof facade.getConfigYamlPath).toBe('function');
|
||||||
|
expect(typeof facade.getConfigJsonPath).toBe('function');
|
||||||
|
expect(typeof facade.hasUnifiedConfig).toBe('function');
|
||||||
|
expect(typeof facade.hasLegacyConfig).toBe('function');
|
||||||
|
expect(typeof facade.getConfigFormat).toBe('function');
|
||||||
|
expect(typeof facade.isUnifiedMode).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export all profile getters', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
expect(typeof facade.getDefaultProfile).toBe('function');
|
||||||
|
expect(typeof facade.setDefaultProfile).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export all section getters', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
expect(typeof facade.getWebSearchConfig).toBe('function');
|
||||||
|
expect(typeof facade.getGlobalEnvConfig).toBe('function');
|
||||||
|
expect(typeof facade.getContinuityInheritanceMap).toBe('function');
|
||||||
|
expect(typeof facade.getCliproxySafetyConfig).toBe('function');
|
||||||
|
expect(typeof facade.getThinkingConfig).toBe('function');
|
||||||
|
expect(typeof facade.getOfficialChannelsConfig).toBe('function');
|
||||||
|
expect(typeof facade.isDashboardAuthEnabled).toBe('function');
|
||||||
|
expect(typeof facade.getDashboardAuthConfig).toBe('function');
|
||||||
|
expect(typeof facade.getBrowserConfig).toBe('function');
|
||||||
|
expect(typeof facade.getImageAnalysisConfig).toBe('function');
|
||||||
|
expect(typeof facade.getLoggingConfig).toBe('function');
|
||||||
|
expect(typeof facade.getCursorConfig).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('re-exports from config-manager', () => {
|
||||||
|
it('should export loadSettings, loadConfigSafe, readConfig, getCcsDir', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
expect(typeof facade.loadSettings).toBe('function');
|
||||||
|
expect(typeof facade.loadConfigSafe).toBe('function');
|
||||||
|
expect(typeof facade.readConfig).toBe('function');
|
||||||
|
expect(typeof facade.getCcsDir).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('memoization', () => {
|
||||||
|
it('getCachedConfig returns same object on repeated calls', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const first = facade.getCachedConfig();
|
||||||
|
const second = facade.getCachedConfig();
|
||||||
|
|
||||||
|
// Same reference (cached, not re-read)
|
||||||
|
expect(first).toBe(second);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalidateConfigCache forces re-read on next getCachedConfig', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const first = facade.getCachedConfig();
|
||||||
|
facade.invalidateConfigCache();
|
||||||
|
const second = facade.getCachedConfig();
|
||||||
|
|
||||||
|
// Different reference after invalidation
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
// But same content
|
||||||
|
expect(first.version).toBe(second.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveConfig updates cache and does not invalidate', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const config = facade.getCachedConfig();
|
||||||
|
config.default = 'test-profile';
|
||||||
|
facade.saveConfig(config);
|
||||||
|
|
||||||
|
const cached = facade.getCachedConfig();
|
||||||
|
// Cache should hold the just-saved config (no re-read)
|
||||||
|
expect(cached).toBe(config);
|
||||||
|
expect(cached.default).toBe('test-profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mutateConfig invalidates the cache', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const before = facade.getCachedConfig();
|
||||||
|
facade.mutateConfig((cfg) => {
|
||||||
|
cfg.default = 'mutated-profile';
|
||||||
|
});
|
||||||
|
const after = facade.getCachedConfig();
|
||||||
|
|
||||||
|
// Different reference (mutator may change it arbitrarily)
|
||||||
|
expect(before).not.toBe(after);
|
||||||
|
expect(after.default).toBe('mutated-profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updateConfig invalidates the cache', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const before = facade.getCachedConfig();
|
||||||
|
facade.updateConfig({ default: 'updated-profile' });
|
||||||
|
const after = facade.getCachedConfig();
|
||||||
|
|
||||||
|
expect(before).not.toBe(after);
|
||||||
|
expect(after.default).toBe('updated-profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCachedConfig returns valid config with expected fields', async () => {
|
||||||
|
const facade = await importFacade();
|
||||||
|
|
||||||
|
const config = facade.getCachedConfig();
|
||||||
|
expect(config).toBeDefined();
|
||||||
|
expect(typeof config.version).toBe('number');
|
||||||
|
expect(config.accounts).toBeDefined();
|
||||||
|
expect(config.profiles).toBeDefined();
|
||||||
|
expect(config.cliproxy).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Config Loader Facade
|
||||||
|
*
|
||||||
|
* Single import path for all config loading operations.
|
||||||
|
* Re-exports everything from unified-config-loader and config-manager,
|
||||||
|
* and adds memoization for loadOrCreateUnifiedConfig to reduce file I/O.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { getCachedConfig, saveConfig, mutateConfig } from '../config/config-loader-facade';
|
||||||
|
* import { getCcsDir, loadSettings } from '../config/config-loader-facade';
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export all functions from unified-config-loader
|
||||||
|
export {
|
||||||
|
loadUnifiedConfig,
|
||||||
|
loadOrCreateUnifiedConfig,
|
||||||
|
saveUnifiedConfig,
|
||||||
|
mutateUnifiedConfig,
|
||||||
|
updateUnifiedConfig,
|
||||||
|
getConfigYamlPath,
|
||||||
|
getConfigJsonPath,
|
||||||
|
hasUnifiedConfig,
|
||||||
|
hasLegacyConfig,
|
||||||
|
getConfigFormat,
|
||||||
|
isUnifiedMode,
|
||||||
|
getDefaultProfile,
|
||||||
|
setDefaultProfile,
|
||||||
|
getWebSearchConfig,
|
||||||
|
getGlobalEnvConfig,
|
||||||
|
getContinuityInheritanceMap,
|
||||||
|
getCliproxySafetyConfig,
|
||||||
|
getThinkingConfig,
|
||||||
|
getOfficialChannelsConfig,
|
||||||
|
isDashboardAuthEnabled,
|
||||||
|
getDashboardAuthConfig,
|
||||||
|
getBrowserConfig,
|
||||||
|
getImageAnalysisConfig,
|
||||||
|
getLoggingConfig,
|
||||||
|
getCursorConfig,
|
||||||
|
} from './unified-config-loader';
|
||||||
|
|
||||||
|
// Re-export types from unified-config-loader
|
||||||
|
export type { GeminiWebSearchInfo } from './unified-config-loader';
|
||||||
|
|
||||||
|
// Re-export selected functions from config-manager
|
||||||
|
export { loadSettings, loadConfigSafe, readConfig, getCcsDir } from '../utils/config-manager';
|
||||||
|
|
||||||
|
// Internal imports for memoization wrappers
|
||||||
|
import type { UnifiedConfig } from './unified-config-types';
|
||||||
|
import {
|
||||||
|
loadOrCreateUnifiedConfig as _loadOrCreateUnifiedConfig,
|
||||||
|
saveUnifiedConfig as _saveUnifiedConfig,
|
||||||
|
mutateUnifiedConfig as _mutateUnifiedConfig,
|
||||||
|
updateUnifiedConfig as _updateUnifiedConfig,
|
||||||
|
} from './unified-config-loader';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Memoization cache
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let _configCache: UnifiedConfig | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unified config with in-memory caching.
|
||||||
|
* First call reads from disk; subsequent calls return the cached object.
|
||||||
|
*
|
||||||
|
* Call invalidateConfigCache() or use mutateConfig()/updateConfig()
|
||||||
|
* to force a re-read from disk.
|
||||||
|
*/
|
||||||
|
export function getCachedConfig(): UnifiedConfig {
|
||||||
|
if (!_configCache) {
|
||||||
|
_configCache = _loadOrCreateUnifiedConfig();
|
||||||
|
}
|
||||||
|
return _configCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the memoization cache.
|
||||||
|
* The next call to getCachedConfig() will re-read from disk.
|
||||||
|
*/
|
||||||
|
export function invalidateConfigCache(): void {
|
||||||
|
_configCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save config to disk and update the cache to the given object.
|
||||||
|
* Does NOT invalidate — the provided config IS the new cache value.
|
||||||
|
*/
|
||||||
|
export function saveConfig(config: UnifiedConfig): void {
|
||||||
|
_saveUnifiedConfig(config);
|
||||||
|
_configCache = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically mutate config (read-modify-write with lock) and invalidate cache.
|
||||||
|
* After mutation, the next getCachedConfig() call will re-read from disk.
|
||||||
|
*/
|
||||||
|
export function mutateConfig(mutator: (config: UnifiedConfig) => void): UnifiedConfig {
|
||||||
|
const result = _mutateUnifiedConfig(mutator);
|
||||||
|
_configCache = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Partial-update config and invalidate cache.
|
||||||
|
* Shorthand for mutateConfig with Object.assign.
|
||||||
|
*/
|
||||||
|
export function updateConfig(updates: Partial<UnifiedConfig>): UnifiedConfig {
|
||||||
|
const result = _updateUnifiedConfig(updates);
|
||||||
|
_configCache = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current cache state (for diagnostics/testing).
|
||||||
|
* Returns true if a cached config exists, false otherwise.
|
||||||
|
*/
|
||||||
|
export function hasCachedConfig(): boolean {
|
||||||
|
return _configCache !== null;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user