mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
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.
421 lines
14 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|