fix(cliproxy): preserve user API keys during config regeneration

When CCS updates trigger CLIProxy config regeneration, user-added API
keys in ~/.ccs/cliproxy/config.yaml were being overwritten with only
the internal CCS key.

Changes:
- Add parseUserApiKeys() to extract user keys from existing config
- Modify regenerateConfig() to preserve user API keys alongside port
- Update generateUnifiedConfigContent() to accept userApiKeys param
- Add comprehensive test suite (14 tests) for key preservation

Fixes #200
This commit is contained in:
kaitranntt committed 2025-12-24 22:12:09 -05:00
1 parent 7dafed663b
commit 2b4d21e8ae
3 files changed
+435 -9

No files matched your search

@@ -166,6 +166,365 @@ describe('Config Generator', () => {
});
});
// =========================================================================
// API Key Preservation Tests (Issue #200)
// =========================================================================
describe('parseUserApiKeys', () => {
let parseUserApiKeys;
beforeEach(() => {
// Clear cache and reload module
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
parseUserApiKeys = configGenerator.parseUserApiKeys;
});
it('extracts user-added API keys from config content', () => {
const configContent = `
# CLIProxyAPI config generated by CCS v4
port: 8317
api-keys:
- "ccs-internal-managed"
- "user-custom-key-12345"
- "another-user-key-abcde"
auth-dir: "/home/user/.ccs/cliproxy/auth"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['user-custom-key-12345', 'another-user-key-abcde']);
});
it('excludes the internal CCS key', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "my-custom-key"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.strictEqual(userKeys.length, 1);
assert.strictEqual(userKeys[0], 'my-custom-key');
assert(!userKeys.includes('ccs-internal-managed'), 'Should not include internal key');
});
it('returns empty array when only internal key exists', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, []);
});
it('returns empty array when no api-keys section exists', () => {
const configContent = `
port: 8317
debug: false
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, []);
});
it('handles single user key', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "single-user-key"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['single-user-key']);
});
it('handles multiple user keys', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "key1"
- "key2"
- "key3"
- "key4"
- "key5"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['key1', 'key2', 'key3', 'key4', 'key5']);
});
it('handles keys with special characters', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "sk_live_1234567890abcdef"
- "api-key-with-dashes"
- "key_with_underscores_123"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, [
'sk_live_1234567890abcdef',
'api-key-with-dashes',
'key_with_underscores_123',
]);
});
it('preserves key order', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "zebra-key"
- "alpha-key"
- "middle-key"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['zebra-key', 'alpha-key', 'middle-key']);
});
it('handles empty string key (edge case)', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- ""
- "valid-key"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
// Empty strings should be filtered out (only truthy values)
assert.deepStrictEqual(userKeys, ['valid-key']);
});
it('handles config with Windows line endings', () => {
const configContent =
'api-keys:\r\n - "ccs-internal-managed"\r\n - "user-key"\r\n\r\nauth-dir: "/test"\r\n';
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['user-key']);
});
it('handles different indentation styles', () => {
const configContent = `
api-keys:
- "ccs-internal-managed"
- "user-key-with-4-spaces"
auth-dir: "/test"
`;
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['user-key-with-4-spaces']);
});
it('handles tabs in indentation', () => {
const configContent = 'api-keys:\n\t- "ccs-internal-managed"\n\t- "user-key-with-tab"\n\nauth-dir: "/test"';
const userKeys = parseUserApiKeys(configContent);
assert.deepStrictEqual(userKeys, ['user-key-with-tab']);
});
});
describe('regenerateConfig API key preservation', () => {
const fs = require('fs');
const os = require('os');
const path = require('path');
let testDir;
let originalCcsHome;
let regenerateConfig;
let getConfigPath;
beforeEach(() => {
// Create a temporary test directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = testDir;
// Clear cache and reload module
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
const configGenerator = require('../../../dist/cliproxy/config-generator');
regenerateConfig = configGenerator.regenerateConfig;
getConfigPath = configGenerator.getConfigPath;
});
afterEach(() => {
// Restore environment
process.env.CCS_HOME = originalCcsHome;
// Clean up test directory
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
it('preserves user API keys during regeneration', () => {
// Create initial config with user keys
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 8317
api-keys:
- "ccs-internal-managed"
- "user-custom-key-12345"
- "another-user-key"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
// Regenerate config (simulating CCS update)
regenerateConfig();
// Read regenerated config
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// Verify user keys are preserved
assert(newConfig.includes('user-custom-key-12345'), 'Should preserve first user key');
assert(newConfig.includes('another-user-key'), 'Should preserve second user key');
assert(newConfig.includes('ccs-internal-managed'), 'Should include internal key');
});
it('preserves port setting during regeneration', () => {
// Create initial config with custom port
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 9999
api-keys:
- "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
// Regenerate config
regenerateConfig();
// Read regenerated config
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// Verify port is preserved
assert(newConfig.includes('port: 9999'), 'Should preserve custom port');
});
it('creates fresh config when none exists', () => {
// Ensure clean state
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
const configPath = path.join(cliproxyDir, 'config.yaml');
assert(!fs.existsSync(configPath), 'Config should not exist initially');
// Generate config
regenerateConfig();
// Verify config was created
assert(fs.existsSync(configPath), 'Config should be created');
const config = fs.readFileSync(configPath, 'utf-8');
assert(config.includes('ccs-internal-managed'), 'Should include internal key');
assert(config.includes('port: 8317'), 'Should use default port');
});
it('handles corrupted config gracefully', () => {
// Create corrupted config
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), 'invalid yaml content: [[[');
// Should not throw
assert.doesNotThrow(() => regenerateConfig());
// Should create valid config
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
});
it('handles empty config file gracefully', () => {
// Create empty config
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), '');
// Should not throw
assert.doesNotThrow(() => regenerateConfig());
// Should create valid config with defaults
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
assert(newConfig.includes('port: 8317'), 'Should use default port');
});
it('preserves multiple user keys in correct order', () => {
// Create config with multiple user keys
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 8317
api-keys:
- "ccs-internal-managed"
- "key-first"
- "key-second"
- "key-third"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
// Regenerate config
regenerateConfig();
// Read regenerated config
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
// Verify order is preserved (first > second > third)
const firstPos = newConfig.indexOf('key-first');
const secondPos = newConfig.indexOf('key-second');
const thirdPos = newConfig.indexOf('key-third');
assert(firstPos < secondPos, 'First key should come before second');
assert(secondPos < thirdPos, 'Second key should come before third');
});
});
describe('Cross-platform path handling', () => {
it('normalizes paths regardless of current platform', () => {
// The fix should work consistently on Windows, macOS, and Linux