/** * Variant Port Edge Case Tests * * Tests for edge cases and error handling in variant port isolation. * Covers port exhaustion, race conditions, stale session cleanup, * legacy migration, permission errors, and config corruption. */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); // Set test isolation environment before importing const testHome = path.join( os.tmpdir(), `ccs-test-edge-${Date.now()}-${Math.random().toString(36).slice(2)}` ); process.env.CCS_HOME = testHome; const { getNextAvailablePort, VARIANT_PORT_BASE, VARIANT_PORT_MAX_OFFSET, listVariantsFromConfig, saveVariantLegacy, removeVariantFromLegacyConfig, } = require('../../../dist/cliproxy/services/variant-config-adapter'); const { getExistingProxy, registerSession, unregisterSession, cleanupOrphanedSessions, deleteSessionLockForPort, getProxyStatus, } = require('../../../dist/cliproxy/session-tracker'); const { deleteConfigForPort, configExists, generateConfig, } = require('../../../dist/cliproxy/config-generator'); describe('Variant Port Edge Cases', function () { let configPath; let cliproxyDir; beforeEach(function () { // Create test directories const ccsDir = path.join(testHome, '.ccs'); cliproxyDir = path.join(ccsDir, 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true }); configPath = path.join(ccsDir, 'config.json'); // Start with empty config fs.writeFileSync(configPath, JSON.stringify({ profiles: {} })); }); afterEach(function () { // Clean up config and session files try { if (fs.existsSync(configPath)) { fs.unlinkSync(configPath); } const files = fs.readdirSync(cliproxyDir); for (const file of files) { fs.unlinkSync(path.join(cliproxyDir, file)); } } catch { // Ignore cleanup errors } }); afterAll(function () { // Clean up test directory try { fs.rmSync(testHome, { recursive: true, force: true }); } catch { // Ignore cleanup errors } delete process.env.CCS_HOME; }); describe('Port Exhaustion', function () { it('throws after 100 variants created', function () { const ccsDir = path.join(testHome, '.ccs'); // Create 100 variants for (let i = 0; i < 100; i++) { const settingsPath = path.join(ccsDir, `variant${i}.settings.json`); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, VARIANT_PORT_BASE + i); } assert.throws(() => getNextAvailablePort(), /Port limit reached/); }); it('error message shows 100/100 and recovery hint', function () { const ccsDir = path.join(testHome, '.ccs'); for (let i = 0; i < 100; i++) { const settingsPath = path.join(ccsDir, `variant${i}.settings.json`); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, VARIANT_PORT_BASE + i); } try { getNextAvailablePort(); assert.fail('Should have thrown'); } catch (err) { assert.ok(err.message.includes('100/100')); assert.ok(err.message.includes('ccs cliproxy remove')); } }); it('frees port after variant removal', function () { const ccsDir = path.join(testHome, '.ccs'); // Create variant on port 8318 const settingsPath = path.join(ccsDir, 'test.settings.json'); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy('test', 'gemini', settingsPath, undefined, VARIANT_PORT_BASE); // Next should be 8319 assert.strictEqual(getNextAvailablePort(), VARIANT_PORT_BASE + 1); // Remove variant removeVariantFromLegacyConfig('test'); // 8318 should be free again assert.strictEqual(getNextAvailablePort(), VARIANT_PORT_BASE); }); }); describe('Stale Session Cleanup', function () { it('cleans orphaned session lock on variant delete', function () { const port = 8318; // Create session file registerSession(port, process.pid); const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); assert.ok(fs.existsSync(sessionPath)); // Simulate variant delete cleanup deleteSessionLockForPort(port); assert.strictEqual(fs.existsSync(sessionPath), false); }); it('cleans stale lock when PID not running', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Create lock with dead PID fs.writeFileSync( sessionPath, JSON.stringify({ port, pid: 999999999, // Dead PID sessions: ['session1'], startedAt: new Date().toISOString(), }) ); // getExistingProxy should clean it up const lock = getExistingProxy(port); assert.strictEqual(lock, null); assert.strictEqual(fs.existsSync(sessionPath), false); }); }); describe('Legacy Variant Migration', function () { it('variant without port gets undefined in listing', function () { // Create legacy variant without port const config = { profiles: {}, cliproxy: { legacy: { provider: 'gemini', settings: '/path/to/settings.json', // No port field }, }, }; fs.writeFileSync(configPath, JSON.stringify(config)); const variants = listVariantsFromConfig(); assert.strictEqual(variants.legacy.port, undefined); }); it('legacy variant does not block port allocation', function () { // Create legacy variant without port const config = { profiles: {}, cliproxy: { legacy: { provider: 'gemini', settings: '/path/to/settings.json', // No port field - doesn't count toward port usage }, }, }; fs.writeFileSync(configPath, JSON.stringify(config)); // First port should still be available const port = getNextAvailablePort(); assert.strictEqual(port, VARIANT_PORT_BASE); }); }); describe('Config Corruption', function () { it('handles malformed sessions-{port}.json gracefully', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Write invalid JSON fs.writeFileSync(sessionPath, '{ invalid json }'); // getExistingProxy should return null, not throw const lock = getExistingProxy(port); assert.strictEqual(lock, null); }); it('handles missing config-{port}.yaml gracefully', function () { const port = 8318; // configExists should return false, not throw assert.strictEqual(configExists(port), false); }); it('handles sessions file with missing required fields', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Write JSON missing required fields fs.writeFileSync(sessionPath, JSON.stringify({ port: 8318 })); // getExistingProxy should return null (invalid structure) const lock = getExistingProxy(port); assert.strictEqual(lock, null); }); it('handles sessions file with wrong data types', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Write JSON with wrong types fs.writeFileSync( sessionPath, JSON.stringify({ port: 'not-a-number', pid: 'also-not-a-number', sessions: 'not-an-array', }) ); const lock = getExistingProxy(port); assert.strictEqual(lock, null); }); }); describe('Cleanup on Crash', function () { it('getExistingProxy cleans stale lock if PID dead', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Create lock with dead PID (simulating crashed proxy) fs.writeFileSync( sessionPath, JSON.stringify({ port, pid: 999999999, sessions: ['session1'], startedAt: new Date().toISOString(), }) ); const lock = getExistingProxy(port); assert.strictEqual(lock, null); assert.strictEqual(fs.existsSync(sessionPath), false); }); it('cleanupOrphanedSessions removes stale lock', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Create lock with dead PID fs.writeFileSync( sessionPath, JSON.stringify({ port, pid: 999999999, sessions: ['session1'], startedAt: new Date().toISOString(), }) ); cleanupOrphanedSessions(port); assert.strictEqual(fs.existsSync(sessionPath), false); }); it('variant delete cleans session lock even if proxy crashed', function () { const port = 8318; const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); // Create lock with dead PID fs.writeFileSync( sessionPath, JSON.stringify({ port, pid: 999999999, sessions: ['session1'], startedAt: new Date().toISOString(), }) ); // deleteSessionLockForPort is called during variant removal deleteSessionLockForPort(port); assert.strictEqual(fs.existsSync(sessionPath), false); }); }); describe('Variant Lifecycle Integration', function () { it('creates variant with unique port and separate files', function () { const ccsDir = path.join(testHome, '.ccs'); const port = getNextAvailablePort(); // Create variant const settingsPath = path.join(ccsDir, 'test.settings.json'); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy('test', 'gemini', settingsPath, undefined, port); // Generate config generateConfig('gemini', port); // Verify files exist assert.ok(configExists(port)); // Start session const sessionId = registerSession(port, process.pid); const status = getProxyStatus(port); assert.strictEqual(status.running, true); assert.strictEqual(status.port, port); // Clean up session unregisterSession(sessionId, port); }); it('removes variant and cleans up all port files', function () { const ccsDir = path.join(testHome, '.ccs'); const port = 8318; // Create variant const settingsPath = path.join(ccsDir, 'test.settings.json'); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy('test', 'gemini', settingsPath, undefined, port); // Generate config and session generateConfig('gemini', port); registerSession(port, process.pid); // Verify files exist assert.ok(configExists(port)); assert.strictEqual(getProxyStatus(port).running, true); // Remove variant (simulating full cleanup) removeVariantFromLegacyConfig('test'); deleteConfigForPort(port); deleteSessionLockForPort(port); // Verify cleanup assert.strictEqual(configExists(port), false); assert.strictEqual(getProxyStatus(port).running, false); }); it('port reuse after deletion does not have stale data', function () { const ccsDir = path.join(testHome, '.ccs'); // Create variant A const settingsA = path.join(ccsDir, 'variantA.settings.json'); fs.writeFileSync(settingsA, JSON.stringify({ env: { KEY: 'A' } })); const portA = getNextAvailablePort(); saveVariantLegacy('variantA', 'gemini', settingsA, undefined, portA); generateConfig('gemini', portA); registerSession(portA, process.pid); // Remove variant A with full cleanup removeVariantFromLegacyConfig('variantA'); deleteConfigForPort(portA); deleteSessionLockForPort(portA); // Create variant B - should get same port const settingsB = path.join(ccsDir, 'variantB.settings.json'); fs.writeFileSync(settingsB, JSON.stringify({ env: { KEY: 'B' } })); const portB = getNextAvailablePort(); assert.strictEqual(portB, portA); // Port should be reused // New session should start fresh saveVariantLegacy('variantB', 'gemini', settingsB, undefined, portB); generateConfig('gemini', portB); const sessionB = registerSession(portB, process.pid); const status = getProxyStatus(portB); assert.strictEqual(status.running, true); assert.strictEqual(status.sessionCount, 1); // Clean up unregisterSession(sessionB, portB); }); }); describe('Multiple Concurrent Variants', function () { it('creates 3 variants with different ports', function () { const ccsDir = path.join(testHome, '.ccs'); const ports = []; for (let i = 0; i < 3; i++) { const port = getNextAvailablePort(); ports.push(port); const settingsPath = path.join(ccsDir, `variant${i}.settings.json`); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, port); } // Verify all ports are different const uniquePorts = new Set(ports); assert.strictEqual(uniquePorts.size, 3); // Verify sequential assignment assert.strictEqual(ports[0], VARIANT_PORT_BASE); assert.strictEqual(ports[1], VARIANT_PORT_BASE + 1); assert.strictEqual(ports[2], VARIANT_PORT_BASE + 2); }); it('each has separate config file', function () { const ccsDir = path.join(testHome, '.ccs'); const ports = [8318, 8319, 8320]; for (let i = 0; i < 3; i++) { const settingsPath = path.join(ccsDir, `variant${i}.settings.json`); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, ports[i]); generateConfig('gemini', ports[i]); } // Verify separate config files for (const port of ports) { assert.ok(configExists(port), `Config for port ${port} should exist`); } }); it('each has separate sessions file when running', function () { const ports = [8318, 8319, 8320]; for (const port of ports) { registerSession(port, process.pid); } // Verify separate session files for (const port of ports) { const sessionPath = path.join(cliproxyDir, `sessions-${port}.json`); assert.ok(fs.existsSync(sessionPath), `Session file for port ${port} should exist`); } // Clean up for (const port of ports) { deleteSessionLockForPort(port); } }); it('removing one does not affect others', function () { const ccsDir = path.join(testHome, '.ccs'); const ports = [8318, 8319, 8320]; // Create 3 variants for (let i = 0; i < 3; i++) { const settingsPath = path.join(ccsDir, `variant${i}.settings.json`); fs.writeFileSync(settingsPath, JSON.stringify({ env: {} })); saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, ports[i]); generateConfig('gemini', ports[i]); registerSession(ports[i], process.pid); } // Remove middle variant removeVariantFromLegacyConfig('variant1'); deleteConfigForPort(ports[1]); deleteSessionLockForPort(ports[1]); // Verify others still exist assert.ok(configExists(ports[0])); assert.ok(!configExists(ports[1])); // Removed assert.ok(configExists(ports[2])); assert.strictEqual(getProxyStatus(ports[0]).running, true); assert.strictEqual(getProxyStatus(ports[1]).running, false); // Removed assert.strictEqual(getProxyStatus(ports[2]).running, true); // Clean up remaining deleteSessionLockForPort(ports[0]); deleteSessionLockForPort(ports[2]); }); }); });