Files
ccs/tests/unit/cliproxy/session-tracker-port.test.js
T
kaitranntt 0bcaf4bc68 feat(cliproxy): add variant port isolation for concurrent proxy instances
Enables running multiple CLIProxy variants simultaneously on different
ports. Each variant now gets a unique port in the 18100-18199 range,
allowing concurrent use of providers like Gemini + Codex + custom variants.

Key changes:
- Add port field to variant config schema
- Implement automatic port allocation (18100 + variant index)
- Support variant-specific settings paths in config generator
- Display port in dashboard UI for debugging
- Show all models in variant editor dropdown
- Add comprehensive tests for port allocation edge cases

Closes related variant isolation work.
2025-12-24 04:15:04 -05:00

421 lines
14 KiB
JavaScript

/**
* Session Tracker Port-Specific Tests
*
* Tests for per-port session tracking in session-tracker.ts.
* Verifies port-specific session files (sessions-{port}.json) and cleanup.
*/
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-session-port-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
process.env.CCS_HOME = testHome;
const {
getExistingProxy,
registerSession,
unregisterSession,
cleanupOrphanedSessions,
stopProxy,
getProxyStatus,
getSessionLockPath,
deleteSessionLockForPort,
} = require('../../../dist/cliproxy/session-tracker');
const { CLIPROXY_DEFAULT_PORT } = require('../../../dist/cliproxy/config-generator');
describe('Session Tracker Port-Specific', function () {
const variantPort1 = 8318;
const variantPort2 = 8319;
let cliproxyDir;
beforeEach(function () {
// Create test directories
cliproxyDir = path.join(testHome, '.ccs', 'cliproxy');
fs.mkdirSync(cliproxyDir, { recursive: true });
// Clean up any existing session files
const files = fs.readdirSync(cliproxyDir);
for (const file of files) {
if (file.startsWith('sessions')) {
fs.unlinkSync(path.join(cliproxyDir, file));
}
}
});
afterEach(function () {
// Clean up session files
try {
const files = fs.readdirSync(cliproxyDir);
for (const file of files) {
if (file.startsWith('sessions')) {
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('Session Lock Path', function () {
it('returns sessions.json for default port', function () {
const lockPath = getSessionLockPath();
assert.ok(lockPath.endsWith('sessions.json'));
assert.ok(!lockPath.includes('sessions-'));
});
});
describe('Port-Specific Session Files', function () {
it('creates sessions-{port}.json for variant ports', function () {
registerSession(variantPort1, process.pid);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
assert.ok(fs.existsSync(lockPath), `Should create sessions-${variantPort1}.json`);
});
it('creates sessions.json for default port', function () {
registerSession(CLIPROXY_DEFAULT_PORT, process.pid);
const lockPath = path.join(cliproxyDir, 'sessions.json');
assert.ok(fs.existsSync(lockPath), 'Should create sessions.json for default port');
});
it('keeps separate session files for different ports', function () {
// Register sessions on different ports
registerSession(variantPort1, process.pid);
registerSession(variantPort2, process.pid);
registerSession(CLIPROXY_DEFAULT_PORT, process.pid);
// All three should exist
assert.ok(
fs.existsSync(path.join(cliproxyDir, `sessions-${variantPort1}.json`)),
'Should have port 8318 sessions'
);
assert.ok(
fs.existsSync(path.join(cliproxyDir, `sessions-${variantPort2}.json`)),
'Should have port 8319 sessions'
);
assert.ok(
fs.existsSync(path.join(cliproxyDir, 'sessions.json')),
'Should have default port sessions'
);
});
});
describe('registerSession with Port', function () {
it('stores correct port in session lock file', function () {
registerSession(variantPort1, process.pid);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
assert.strictEqual(lock.port, variantPort1);
});
it('stores correct PID in session lock file', function () {
registerSession(variantPort1, process.pid);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
assert.strictEqual(lock.pid, process.pid);
});
it('appends to existing sessions array for same port', function () {
const session1 = registerSession(variantPort1, process.pid);
const session2 = registerSession(variantPort1, process.pid);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
assert.strictEqual(lock.sessions.length, 2);
assert.ok(lock.sessions.includes(session1));
assert.ok(lock.sessions.includes(session2));
});
});
describe('unregisterSession with Port', function () {
it('removes session from port-specific file', function () {
const session1 = registerSession(variantPort1, process.pid);
const session2 = registerSession(variantPort1, process.pid);
// Unregister first session with port
unregisterSession(session1, variantPort1);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
assert.strictEqual(lock.sessions.length, 1);
assert.strictEqual(lock.sessions[0], session2);
});
it('deletes lock file when last session removed', function () {
const session = registerSession(variantPort1, process.pid);
const shouldKill = unregisterSession(session, variantPort1);
assert.strictEqual(shouldKill, true);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
assert.strictEqual(fs.existsSync(lockPath), false);
});
it('returns true when last session', function () {
const session = registerSession(variantPort1, process.pid);
const shouldKill = unregisterSession(session, variantPort1);
assert.strictEqual(shouldKill, true);
});
it('returns false when sessions remain', function () {
const session1 = registerSession(variantPort1, process.pid);
registerSession(variantPort1, process.pid);
const shouldKill = unregisterSession(session1, variantPort1);
assert.strictEqual(shouldKill, false);
});
it('searches default port for backward compat (fallback)', function () {
// Register on default port
const session = registerSession(CLIPROXY_DEFAULT_PORT, process.pid);
// Unregister without port (should search default)
const shouldKill = unregisterSession(session);
assert.strictEqual(shouldKill, true);
const lockPath = path.join(cliproxyDir, 'sessions.json');
assert.strictEqual(fs.existsSync(lockPath), false);
});
});
describe('getExistingProxy with Port', function () {
it('returns lock for running proxy on specified port', function () {
registerSession(variantPort1, process.pid);
const lock = getExistingProxy(variantPort1);
assert.notStrictEqual(lock, null);
assert.strictEqual(lock.port, variantPort1);
assert.strictEqual(lock.pid, process.pid);
});
it('returns null if lock file missing', function () {
const lock = getExistingProxy(variantPort1);
assert.strictEqual(lock, null);
});
it('returns null if port mismatch', function () {
// Create lock with different port number in file
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: 9999, // Wrong port
pid: process.pid,
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
const lock = getExistingProxy(variantPort1);
assert.strictEqual(lock, null);
});
it('cleans up stale lock if PID not running', function () {
// Create lock with dead PID
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: variantPort1,
pid: 999999999, // Dead PID
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
const lock = getExistingProxy(variantPort1);
assert.strictEqual(lock, null);
assert.strictEqual(fs.existsSync(lockPath), false);
});
});
describe('deleteSessionLockForPort', function () {
it('removes sessions-{port}.json for specified port', function () {
registerSession(variantPort1, process.pid);
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
assert.ok(fs.existsSync(lockPath));
deleteSessionLockForPort(variantPort1);
assert.strictEqual(fs.existsSync(lockPath), false);
});
it('does nothing if file does not exist', function () {
// Should not throw
deleteSessionLockForPort(variantPort1);
});
it('does not affect other port sessions', function () {
registerSession(variantPort1, process.pid);
registerSession(variantPort2, process.pid);
deleteSessionLockForPort(variantPort1);
const lock1Path = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
const lock2Path = path.join(cliproxyDir, `sessions-${variantPort2}.json`);
assert.strictEqual(fs.existsSync(lock1Path), false);
assert.ok(fs.existsSync(lock2Path));
});
});
describe('cleanupOrphanedSessions with Port', function () {
it('deletes lock if PID not running', function () {
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: variantPort1,
pid: 999999999, // Dead PID
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
cleanupOrphanedSessions(variantPort1);
assert.strictEqual(fs.existsSync(lockPath), false);
});
it('keeps lock if PID still running', function () {
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: variantPort1,
pid: process.pid, // Our process - running
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
cleanupOrphanedSessions(variantPort1);
assert.ok(fs.existsSync(lockPath));
});
});
describe('stopProxy with Port', function () {
it('stops proxy on specified port', async function () {
// Create lock with dead PID (we can't actually stop a real process in tests)
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: variantPort1,
pid: 999999999, // Dead PID
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
const result = await stopProxy(variantPort1);
assert.strictEqual(result.stopped, false);
assert.ok(result.error.includes('not running'));
});
it('cleans up session lock after stop', async function () {
const lockPath = path.join(cliproxyDir, `sessions-${variantPort1}.json`);
fs.writeFileSync(
lockPath,
JSON.stringify({
port: variantPort1,
pid: 999999999, // Dead PID
sessions: ['session1'],
startedAt: new Date().toISOString(),
})
);
await stopProxy(variantPort1);
assert.strictEqual(fs.existsSync(lockPath), false);
});
it('handles already-stopped proxy gracefully', async function () {
const result = await stopProxy(variantPort1);
assert.strictEqual(result.stopped, false);
assert.strictEqual(result.error, 'No active CLIProxy session found');
});
});
describe('getProxyStatus with Port', function () {
it('returns correct status for variant port', function () {
registerSession(variantPort1, process.pid);
const status = getProxyStatus(variantPort1);
assert.strictEqual(status.running, true);
assert.strictEqual(status.port, variantPort1);
assert.strictEqual(status.pid, process.pid);
assert.strictEqual(status.sessionCount, 1);
});
it('returns not running for empty variant port', function () {
const status = getProxyStatus(variantPort1);
assert.strictEqual(status.running, false);
});
});
describe('Concurrent Variant Sessions', function () {
it('manages multiple variant ports independently', function () {
// Start sessions on different ports
const session1 = registerSession(variantPort1, process.pid);
const session2 = registerSession(variantPort2, process.pid);
// Both should be tracked
assert.strictEqual(getProxyStatus(variantPort1).running, true);
assert.strictEqual(getProxyStatus(variantPort2).running, true);
// Unregister one should not affect other
unregisterSession(session1, variantPort1);
assert.strictEqual(getProxyStatus(variantPort1).running, false);
assert.strictEqual(getProxyStatus(variantPort2).running, true);
// Clean up
unregisterSession(session2, variantPort2);
});
it('allows same session workflow on different ports', function () {
// Simulate concurrent variant usage
const port1Session1 = registerSession(variantPort1, process.pid);
const port1Session2 = registerSession(variantPort1, process.pid);
const port2Session1 = registerSession(variantPort2, process.pid);
assert.strictEqual(getProxyStatus(variantPort1).sessionCount, 2);
assert.strictEqual(getProxyStatus(variantPort2).sessionCount, 1);
// Unregister from port1
const shouldKill1 = unregisterSession(port1Session1, variantPort1);
assert.strictEqual(shouldKill1, false); // Still has session2
const shouldKill2 = unregisterSession(port1Session2, variantPort1);
assert.strictEqual(shouldKill2, true); // Last session on port1
// Port2 should still be running
assert.strictEqual(getProxyStatus(variantPort2).running, true);
// Clean up
unregisterSession(port2Session1, variantPort2);
});
});
});