mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
852 lines
29 KiB
JavaScript
852 lines
29 KiB
JavaScript
/**
|
|
* Tests for CLIProxy Config Generator
|
|
* Verifies config.yaml generation including Windows path handling
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
|
|
// Mock the config-manager module to provide test paths
|
|
const originalModule = require.cache[require.resolve('../../../dist/utils/config-manager')];
|
|
|
|
describe('Config Generator', () => {
|
|
let configGenerator;
|
|
let mockCcsDir;
|
|
|
|
beforeEach(() => {
|
|
// Clear the require cache to reload module with fresh mocks
|
|
delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
|
|
|
|
// Set up a temporary test directory as CCS_HOME
|
|
mockCcsDir = '/test/home/.ccs';
|
|
});
|
|
|
|
describe('generateUnifiedConfigContent', () => {
|
|
it('converts Windows backslashes to forward slashes in auth-dir path', () => {
|
|
// Simulate Windows path with backslashes
|
|
const windowsPath = 'C:\\Users\\TestUser\\.ccs\\cliproxy\\auth';
|
|
const normalizedPath = windowsPath.replace(/\\/g, '/');
|
|
|
|
// Verify the replacement works correctly
|
|
assert.strictEqual(normalizedPath, 'C:/Users/TestUser/.ccs/cliproxy/auth');
|
|
assert(!normalizedPath.includes('\\'), 'Path should not contain backslashes');
|
|
});
|
|
|
|
it('handles mixed path separators', () => {
|
|
// Test with mixed Windows and forward slashes
|
|
const mixedPath = 'C:\\Users/TestUser\\.ccs/cliproxy\\auth';
|
|
const normalizedPath = mixedPath.replace(/\\/g, '/');
|
|
|
|
assert.strictEqual(normalizedPath, 'C:/Users/TestUser/.ccs/cliproxy/auth');
|
|
});
|
|
|
|
it('leaves forward slashes unchanged', () => {
|
|
// Test with Unix paths (forward slashes only)
|
|
const unixPath = '/home/testuser/.ccs/cliproxy/auth';
|
|
const normalizedPath = unixPath.replace(/\\/g, '/');
|
|
|
|
assert.strictEqual(normalizedPath, '/home/testuser/.ccs/cliproxy/auth');
|
|
});
|
|
|
|
it('generates valid YAML with normalized paths', () => {
|
|
// Test the actual pattern used in config-generator.ts line 130
|
|
const testAuthDir = 'C:\\Users\\TestUser\\.ccs\\cliproxy\\auth';
|
|
const configLine = `auth-dir: "${testAuthDir.replace(/\\/g, '/')}"`;
|
|
|
|
// Verify the YAML line is properly formatted
|
|
assert(configLine.includes('auth-dir:'), 'Should contain auth-dir key');
|
|
assert(configLine.includes('C:/Users/TestUser'), 'Should have normalized path');
|
|
assert(!configLine.includes('\\'), 'Should not contain backslashes');
|
|
|
|
// Verify it's valid YAML format
|
|
assert.strictEqual(configLine, 'auth-dir: "C:/Users/TestUser/.ccs/cliproxy/auth"');
|
|
});
|
|
|
|
it('handles multiple consecutive backslashes', () => {
|
|
// Edge case: multiple backslashes
|
|
const weirdPath = 'C:\\\\Users\\\\TestUser\\\\.ccs\\\\cliproxy\\\\auth';
|
|
const normalizedPath = weirdPath.replace(/\\/g, '/');
|
|
|
|
assert.strictEqual(normalizedPath, 'C://Users//TestUser//.ccs//cliproxy//auth');
|
|
assert(!normalizedPath.includes('\\'), 'Should not contain backslashes');
|
|
});
|
|
|
|
it('preserves all other characters in path', () => {
|
|
// Test that normalization doesn't affect other characters
|
|
const complexPath = 'D:\\Projects\\ccs-2024\\test\\.ccs\\auth-tokens\\provider.json';
|
|
const normalizedPath = complexPath.replace(/\\/g, '/');
|
|
|
|
assert.strictEqual(
|
|
normalizedPath,
|
|
'D:/Projects/ccs-2024/test/.ccs/auth-tokens/provider.json'
|
|
);
|
|
assert(normalizedPath.includes('Projects'), 'Should preserve directory names');
|
|
assert(normalizedPath.includes('auth-tokens'), 'Should preserve hyphens');
|
|
assert(normalizedPath.includes('provider.json'), 'Should preserve filenames');
|
|
});
|
|
|
|
it('works with environment variable expansion', () => {
|
|
// Simulate path with environment variables
|
|
const envPath = process.env.USERPROFILE || 'C:\\Users\\TestUser';
|
|
const fullPath = path.join(envPath, '.ccs', 'cliproxy', 'auth');
|
|
const normalizedPath = fullPath.replace(/\\/g, '/');
|
|
|
|
// On Windows, path.join creates backslashes, on Unix forward slashes
|
|
// The normalization should ensure forward slashes regardless
|
|
assert(!normalizedPath.includes('\\'), 'Should not contain backslashes after normalization');
|
|
assert(normalizedPath.includes('/.ccs/'), 'Should have normalized CCS path');
|
|
});
|
|
|
|
it('is idempotent - applying normalization twice gives same result', () => {
|
|
const windowsPath = 'C:\\Users\\Test\\.ccs\\auth';
|
|
const normalized1 = windowsPath.replace(/\\/g, '/');
|
|
const normalized2 = normalized1.replace(/\\/g, '/');
|
|
|
|
assert.strictEqual(normalized1, normalized2, 'Should be idempotent');
|
|
assert.strictEqual(normalized2, 'C:/Users/Test/.ccs/auth');
|
|
});
|
|
|
|
it('handles YAML escaping requirements correctly', () => {
|
|
// Windows paths with backslashes can cause YAML parsing issues
|
|
// because backslash is an escape character in YAML strings
|
|
const windowsPath = 'C:\\Users\\test\\.ccs\\auth';
|
|
|
|
// Before fix: would contain backslashes, causing YAML parsing errors
|
|
// After fix: forward slashes, which are safe in YAML
|
|
const yamlUnsafePath = `auth-dir: "${windowsPath}"`;
|
|
const yamlSafePath = `auth-dir: "${windowsPath.replace(/\\/g, '/')}"`;
|
|
|
|
// The fixed version should be YAML-safe (no escape character conflicts)
|
|
assert(yamlSafePath.includes('/'), 'Fixed path should use forward slashes');
|
|
assert.strictEqual(yamlSafePath, 'auth-dir: "C:/Users/test/.ccs/auth"');
|
|
});
|
|
});
|
|
|
|
describe('Path separator consistency', () => {
|
|
it('ensures forward slashes work across all platforms', () => {
|
|
// Forward slashes work on Windows, macOS, and Linux
|
|
const forwardSlashPath = '/home/user/.ccs/cliproxy/auth';
|
|
|
|
// This should work everywhere
|
|
assert.strictEqual(
|
|
forwardSlashPath.replace(/\\/g, '/'),
|
|
'/home/user/.ccs/cliproxy/auth',
|
|
'Forward slashes should be preserved on Unix'
|
|
);
|
|
|
|
const windowsPath = 'C:\\Users\\user\\.ccs\\cliproxy\\auth';
|
|
const normalized = windowsPath.replace(/\\/g, '/');
|
|
assert.strictEqual(
|
|
normalized,
|
|
'C:/Users/user/.ccs/cliproxy/auth',
|
|
'Windows paths should be normalized to forward slashes'
|
|
);
|
|
|
|
// Both formats should be usable with Node.js fs and path modules
|
|
// and CLIProxyAPI's path handling
|
|
});
|
|
|
|
it('avoids YAML escape sequence issues', () => {
|
|
// Backslashes in YAML can cause issues like:
|
|
// - \n being interpreted as newline
|
|
// - \t being interpreted as tab
|
|
// - \u being interpreted as unicode
|
|
// Using forward slashes avoids all these issues
|
|
|
|
const problematicPath = 'C:\\Users\\test\\.ccs\\auth';
|
|
const safePath = problematicPath.replace(/\\/g, '/');
|
|
|
|
// Check for problematic escape sequences
|
|
assert(!safePath.includes('\\n'), 'Should not create \\n sequences');
|
|
assert(!safePath.includes('\\t'), 'Should not create \\t sequences');
|
|
assert(!safePath.includes('\\u'), 'Should not create \\u sequences');
|
|
|
|
// The safe path should be suitable for YAML
|
|
assert.strictEqual(safePath, 'C:/Users/test/.ccs/auth');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// 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 - parseUserApiKeys checks: key && key !== CCS_INTERNAL_API_KEY
|
|
// Empty string is falsy, so it's excluded along with internal key
|
|
assert.strictEqual(userKeys.length, 1, 'Should filter out empty string key');
|
|
assert.deepStrictEqual(userKeys, ['valid-key'], 'Should only include valid non-empty keys');
|
|
});
|
|
|
|
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 getCliproxyConfigPath;
|
|
|
|
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;
|
|
getCliproxyConfigPath = configGenerator.getCliproxyConfigPath;
|
|
});
|
|
|
|
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
|
|
|
|
const testPaths = [
|
|
// Windows-style paths
|
|
{ input: 'C:\\Users\\test\\.ccs\\auth', expected: 'C:/Users/test/.ccs/auth' },
|
|
{ input: 'D:\\Projects\\ccs\\auth', expected: 'D:/Projects/ccs/auth' },
|
|
// Unix-style paths
|
|
{ input: '/home/user/.ccs/auth', expected: '/home/user/.ccs/auth' },
|
|
{ input: '/var/ccs/auth', expected: '/var/ccs/auth' },
|
|
// Network paths
|
|
{ input: '\\\\network\\share\\.ccs\\auth', expected: '//network/share/.ccs/auth' },
|
|
];
|
|
|
|
testPaths.forEach(({ input, expected }) => {
|
|
const normalized = input.replace(/\\/g, '/');
|
|
assert.strictEqual(normalized, expected, `Should normalize "${input}" to "${expected}"`);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('oauth-model-alias fork:true', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
let testDir;
|
|
let originalCcsHome;
|
|
let regenerateConfig;
|
|
|
|
beforeEach(() => {
|
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-fork-'));
|
|
originalCcsHome = process.env.CCS_HOME;
|
|
process.env.CCS_HOME = testDir;
|
|
|
|
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;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env.CCS_HOME = originalCcsHome;
|
|
if (testDir && fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('generates fork:true for Claude model aliases', () => {
|
|
regenerateConfig();
|
|
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
|
|
// Claude aliases should have fork: true
|
|
assert(config.includes('claude-sonnet-4-6'), 'Should include Claude Sonnet 4.6 model');
|
|
assert(config.includes('fork: true'), 'Should include fork: true for Claude aliases');
|
|
assert(
|
|
!config.includes('claude-sonnet-4-5'),
|
|
'Should not include denylisted Claude Sonnet 4.5 aliases'
|
|
);
|
|
assert(
|
|
!config.includes('claude-opus-4-5'),
|
|
'Should not include denylisted Claude Opus 4.5 aliases'
|
|
);
|
|
assert(
|
|
!config.includes('alias: gemini-claude-'),
|
|
'Should not emit deprecated gemini-claude aliases'
|
|
);
|
|
|
|
// Verify fork: true appears after each Claude alias entry
|
|
const lines = config.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].includes('alias: claude-')) {
|
|
assert(
|
|
lines[i + 1] && lines[i + 1].trim() === 'fork: true',
|
|
`fork: true should follow Claude alias at line ${i}: ${lines[i]}`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('does not generate fork:true for non-Claude aliases', () => {
|
|
regenerateConfig();
|
|
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
|
|
// Gemini aliases should NOT have fork: true
|
|
const lines = config.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].includes('alias: gemini-3-') || lines[i].includes('alias: gemini-2.5-')) {
|
|
const nextLine = lines[i + 1] || '';
|
|
assert(
|
|
!nextLine.trim().startsWith('fork:'),
|
|
`Gemini alias should not have fork: ${lines[i]}`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('generates Gemini 3.1 compatibility aliases without fork', () => {
|
|
regenerateConfig();
|
|
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
const lines = config.split('\n');
|
|
|
|
const gemini31AliasLines = [
|
|
'alias: gemini-3.1-pro-preview',
|
|
'alias: gemini-3.1-pro-preview-customtools',
|
|
'alias: gemini-3.1-flash-preview',
|
|
'alias: gemini-3.1-flash-preview-customtools',
|
|
];
|
|
|
|
for (const aliasLine of gemini31AliasLines) {
|
|
const lineIndex = lines.findIndex((line) => line.includes(aliasLine));
|
|
assert(lineIndex >= 0, `Should include ${aliasLine}`);
|
|
const nextLine = lines[lineIndex + 1] || '';
|
|
assert(
|
|
!nextLine.trim().startsWith('fork:'),
|
|
`Gemini alias should not have fork: ${aliasLine}`
|
|
);
|
|
}
|
|
});
|
|
|
|
it('generates dot and hyphen alias forms for preview compatibility', () => {
|
|
regenerateConfig();
|
|
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
|
|
assert(
|
|
config.includes('alias: gemini-3.1-pro-preview'),
|
|
'Should include dotted version alias'
|
|
);
|
|
assert(
|
|
config.includes('alias: gemini-3-1-pro-preview'),
|
|
'Should include hyphen version alias'
|
|
);
|
|
assert(
|
|
config.includes('alias: gemini-3-1-pro-preview-customtools'),
|
|
'Should include hyphen customtools alias'
|
|
);
|
|
});
|
|
|
|
it('preserves multiple aliases that share the same model name', () => {
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
fs.mkdirSync(cliproxyDir, { recursive: true });
|
|
|
|
const initialConfig = `# CLIProxyAPI config generated by CCS v8
|
|
port: 8317
|
|
api-keys:
|
|
- "ccs-internal-managed"
|
|
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
|
oauth-model-alias:
|
|
antigravity:
|
|
- name: custom-model
|
|
alias: custom-alias-a
|
|
- name: custom-model
|
|
alias: custom-alias-b
|
|
`;
|
|
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
|
|
|
regenerateConfig();
|
|
|
|
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
|
|
assert(newConfig.includes('alias: custom-alias-a'), 'Should preserve first alias');
|
|
assert(newConfig.includes('alias: custom-alias-b'), 'Should preserve second alias');
|
|
assert.strictEqual(
|
|
(newConfig.match(/alias: custom-alias-a/g) || []).length,
|
|
1,
|
|
'Should keep one entry for first alias'
|
|
);
|
|
assert.strictEqual(
|
|
(newConfig.match(/alias: custom-alias-b/g) || []).length,
|
|
1,
|
|
'Should keep one entry for second alias'
|
|
);
|
|
});
|
|
|
|
it('does not auto-enrich aliases from cached catalog to avoid model list bloat', () => {
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
const cachePath = path.join(testDir, '.ccs', 'model-catalog-cache.json');
|
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
|
|
regenerateConfig();
|
|
|
|
const beforeCacheConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
assert(
|
|
!beforeCacheConfig.includes('alias: gemini-3.11-pro-preview'),
|
|
'Should not include unseen minor alias before cache enrichment'
|
|
);
|
|
|
|
const cachePayload = {
|
|
providers: {
|
|
agy: [{ id: 'gemini-3.11-pro-preview', display_name: 'Gemini 3.11 Pro Preview' }],
|
|
},
|
|
fetchedAt: Date.now(),
|
|
};
|
|
fs.writeFileSync(cachePath, JSON.stringify(cachePayload));
|
|
|
|
regenerateConfig();
|
|
|
|
const afterCacheConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
assert(
|
|
!afterCacheConfig.includes('alias: gemini-3.11-pro-preview'),
|
|
'Should not include cache-derived unseen minor alias'
|
|
);
|
|
assert(
|
|
!afterCacheConfig.includes('alias: gemini-3.11-pro-preview-customtools'),
|
|
'Should not include compatibility alias for cache-derived entry'
|
|
);
|
|
assert(
|
|
afterCacheConfig.includes('alias: gemini-3-pro-preview'),
|
|
'Should keep baseline Gemini preview alias generation'
|
|
);
|
|
assert(
|
|
afterCacheConfig.includes('alias: gemini-3-pro-preview-customtools'),
|
|
'Should keep baseline compatibility alias generation'
|
|
);
|
|
});
|
|
|
|
it('preserves user-added aliases with fork during regeneration', () => {
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
fs.mkdirSync(cliproxyDir, { recursive: true });
|
|
|
|
const initialConfig = `# CLIProxyAPI config generated by CCS v6
|
|
port: 8317
|
|
api-keys:
|
|
- "ccs-internal-managed"
|
|
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
|
oauth-model-alias:
|
|
antigravity:
|
|
- name: custom-model
|
|
alias: my-custom-alias
|
|
fork: true
|
|
`;
|
|
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
|
|
|
regenerateConfig();
|
|
|
|
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
assert(newConfig.includes('custom-model'), 'Should preserve custom alias name');
|
|
assert(newConfig.includes('my-custom-alias'), 'Should preserve custom alias');
|
|
|
|
// Check fork is preserved for user alias
|
|
const lines = newConfig.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].includes('alias: my-custom-alias')) {
|
|
assert(
|
|
lines[i + 1] && lines[i + 1].trim() === 'fork: true',
|
|
'Should preserve fork: true for user-added alias'
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('normalizes deprecated gemini-claude aliases during regeneration', () => {
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
fs.mkdirSync(cliproxyDir, { recursive: true });
|
|
|
|
const initialConfig = `# CLIProxyAPI config generated by CCS v9
|
|
port: 8317
|
|
api-keys:
|
|
- "ccs-internal-managed"
|
|
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
|
oauth-model-alias:
|
|
antigravity:
|
|
- name: claude-sonnet-4-6-thinking
|
|
alias: gemini-claude-sonnet-4-6-thinking
|
|
fork: true
|
|
`;
|
|
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
|
|
|
regenerateConfig();
|
|
|
|
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
assert(
|
|
newConfig.includes('alias: claude-sonnet-4-6'),
|
|
'Should migrate deprecated sonnet thinking alias to canonical model ID'
|
|
);
|
|
assert(
|
|
!newConfig.includes('alias: gemini-claude-sonnet-4-6-thinking'),
|
|
'Should remove deprecated gemini-claude alias after regeneration'
|
|
);
|
|
});
|
|
|
|
it('normalizes dotted deprecated sonnet thinking aliases during regeneration', () => {
|
|
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
|
fs.mkdirSync(cliproxyDir, { recursive: true });
|
|
|
|
const initialConfig = `# CLIProxyAPI config generated by CCS v11
|
|
port: 8317
|
|
api-keys:
|
|
- "ccs-internal-managed"
|
|
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
|
oauth-model-alias:
|
|
antigravity:
|
|
- name: claude-sonnet-4-6-thinking
|
|
alias: claude-sonnet-4.6-thinking
|
|
fork: true
|
|
`;
|
|
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
|
|
|
regenerateConfig();
|
|
|
|
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
|
assert(
|
|
newConfig.includes('alias: claude-sonnet-4-6'),
|
|
'Should migrate dotted deprecated sonnet thinking alias to canonical model ID'
|
|
);
|
|
assert(
|
|
!newConfig.includes('alias: claude-sonnet-4.6-thinking'),
|
|
'Should remove dotted deprecated sonnet thinking alias after regeneration'
|
|
);
|
|
});
|
|
});
|
|
});
|