diff --git a/tests/unit/cliproxy/session-tracker-target.test.ts b/tests/unit/cliproxy/session-tracker-target.test.ts index 2a8d23ad..be4257e1 100644 --- a/tests/unit/cliproxy/session-tracker-target.test.ts +++ b/tests/unit/cliproxy/session-tracker-target.test.ts @@ -1,56 +1,111 @@ -import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { describe, expect, it } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { - registerSession, - unregisterSession, - getProxyStatus, -} from '../../../src/cliproxy/session-tracker'; +import { spawnSync } from 'child_process'; +import { pathToFileURL } from 'url'; -describe('session-tracker target metadata', () => { - let tmpDir: string; - let originalCcsHome: string | undefined; - const port = 28317; +const REPO_ROOT = path.resolve(import.meta.dir, '../../..'); +const SESSION_TRACKER_URL = pathToFileURL( + path.join(REPO_ROOT, 'src/cliproxy/session-tracker.ts') +).href; - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-session-target-test-')); - originalCcsHome = process.env.CCS_HOME; - process.env.CCS_HOME = tmpDir; - }); +function withScopedSessionTrackerHome(run: (tempHome: string) => T): T { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-session-target-test-')); + try { + return run(tempHome); + } finally { + fs.rmSync(tempHome, { recursive: true, force: true }); + } +} - afterEach(() => { - if (originalCcsHome !== undefined) { - process.env.CCS_HOME = originalCcsHome; - } else { - delete process.env.CCS_HOME; +function runSessionTrackerScenario( + tempHome: string, + targets: string[] +): { + running: boolean; + target?: string; + sessionCount?: number; +} { + const script = ` + import { + registerSession, + unregisterSession, + getProxyStatus, + } from ${JSON.stringify(SESSION_TRACKER_URL)}; + + const port = 28317; + const sessionIds = []; + for (const target of ${JSON.stringify(targets)}) { + sessionIds.push(registerSession(port, process.pid, undefined, undefined, target)); } - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it('returns single target when all sessions share same target', () => { - const s1 = registerSession(port, process.pid, undefined, undefined, 'droid'); - const s2 = registerSession(port, process.pid, undefined, undefined, 'droid'); const status = getProxyStatus(port); - expect(status.running).toBe(true); - expect(status.target).toBe('droid'); - expect(status.sessionCount).toBe(2); + for (const sessionId of sessionIds) { + unregisterSession(sessionId, port); + } - unregisterSession(s1, port); - unregisterSession(s2, port); + console.log(JSON.stringify({ + running: status.running, + target: status.target ?? null, + sessionCount: status.sessionCount ?? null, + })); + `; + + const scriptPath = path.join(tempHome, `session-target-child-${Date.now()}.mjs`); + fs.writeFileSync(scriptPath, script, 'utf8'); + + const result = spawnSync('/bin/bash', ['-lc', `bun ${JSON.stringify(scriptPath)}`], { + cwd: REPO_ROOT, + env: { + ...process.env, + CCS_HOME: tempHome, + CCS_DIR: '', + }, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + + if (result.status !== 0) { + throw new Error( + `child session-tracker scenario failed: ${JSON.stringify({ + command: `bun ${scriptPath}`, + status: result.status, + signal: result.signal, + error: result.error?.message ?? null, + stdout: result.stdout, + stderr: result.stderr, + })}` + ); + } + + const lines = result.stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + return JSON.parse(lines.at(-1) || '{}') as { + running: boolean; + target?: string; + sessionCount?: number; + }; +} + +describe('session-tracker target metadata', () => { + it('returns single target when all sessions share same target', () => { + withScopedSessionTrackerHome((tempHome) => { + const status = runSessionTrackerScenario(tempHome, ['droid', 'droid']); + expect(status.running).toBe(true); + expect(status.target).toBe('droid'); + expect(status.sessionCount).toBe(2); + }); }); it('returns mixed when active sessions use different targets', () => { - const s1 = registerSession(port, process.pid, undefined, undefined, 'claude'); - const s2 = registerSession(port, process.pid, undefined, undefined, 'droid'); - - const status = getProxyStatus(port); - expect(status.running).toBe(true); - expect(status.target).toBe('mixed'); - expect(status.sessionCount).toBe(2); - - unregisterSession(s1, port); - unregisterSession(s2, port); + withScopedSessionTrackerHome((tempHome) => { + const status = runSessionTrackerScenario(tempHome, ['claude', 'droid']); + expect(status.running).toBe(true); + expect(status.target).toBe('mixed'); + expect(status.sessionCount).toBe(2); + }); }); }); diff --git a/tests/unit/commands/tokens-command-auth-rotation.test.ts b/tests/unit/commands/tokens-command-auth-rotation.test.ts index d2c86e8b..1ef6c716 100644 --- a/tests/unit/commands/tokens-command-auth-rotation.test.ts +++ b/tests/unit/commands/tokens-command-auth-rotation.test.ts @@ -2,64 +2,125 @@ import { describe, expect, it } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { handleTokensCommand } from '../../../src/commands/tokens-command'; -import { getConfigYamlPath, loadUnifiedConfig } from '../../../src/config/unified-config-loader'; -import { runWithScopedCcsHome, setGlobalConfigDir } from '../../../src/utils/config-manager'; +import { spawnSync } from 'child_process'; +import { pathToFileURL } from 'url'; +import { setGlobalConfigDir } from '../../../src/utils/config-manager'; -async function withScopedTokensHome(run: (tempHome: string) => Promise): Promise { +const REPO_ROOT = path.resolve(import.meta.dir, '../../..'); +const TOKENS_COMMAND_URL = pathToFileURL( + path.join(REPO_ROOT, 'src/commands/tokens-command.ts') +).href; +const UNIFIED_CONFIG_LOADER_URL = pathToFileURL( + path.join(REPO_ROOT, 'src/config/unified-config-loader.ts') +).href; + +function withScopedTokensHome(run: (tempHome: string) => T): T { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-tokens-rotation-')); setGlobalConfigDir(undefined); try { - return await runWithScopedCcsHome(tempHome, async () => await run(tempHome)); + return run(tempHome); } finally { setGlobalConfigDir(undefined); fs.rmSync(tempHome, { recursive: true, force: true }); } } +function runTokensCommandInChild(tempHome: string, args: string[]) { + const script = ` + import { handleTokensCommand } from ${JSON.stringify(TOKENS_COMMAND_URL)}; + import { loadUnifiedConfig } from ${JSON.stringify(UNIFIED_CONFIG_LOADER_URL)}; + + const exitCode = await handleTokensCommand(${JSON.stringify(args)}); + const config = loadUnifiedConfig(); + const managementSecret = config?.cliproxy.auth?.management_secret ?? null; + + console.log(JSON.stringify({ + exitCode, + apiKey: config?.cliproxy.auth?.api_key ?? null, + managementSecretLength: typeof managementSecret === 'string' ? managementSecret.length : 0, + })); + `; + + const scriptPath = path.join(tempHome, `tokens-child-${Date.now()}.mjs`); + fs.writeFileSync(scriptPath, script, 'utf8'); + + const result = spawnSync('/bin/bash', ['-lc', `bun ${JSON.stringify(scriptPath)}`], { + cwd: REPO_ROOT, + env: { + ...process.env, + CCS_HOME: tempHome, + CCS_DIR: '', + NO_COLOR: '1', + }, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + + if (result.status !== 0) { + throw new Error( + `child tokens command failed: ${JSON.stringify({ + command: `bun ${scriptPath}`, + status: result.status, + signal: result.signal, + error: result.error?.message ?? null, + stdout: result.stdout, + stderr: result.stderr, + })}` + ); + } + + const lines = result.stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + const payload = JSON.parse(lines.at(-1) || '{}') as { + exitCode: number; + apiKey: string | null; + managementSecretLength: number; + }; + + return { payload, stdout: result.stdout, stderr: result.stderr }; +} + describe('tokens command auth rotation', () => { - it('applies api-key and regenerated secret in a single invocation', async () => { - await withScopedTokensHome(async () => { - const exitCode = await handleTokensCommand([ + it('applies api-key and regenerated secret in a single invocation', () => { + withScopedTokensHome((tempHome) => { + const { payload } = runTokensCommandInChild(tempHome, [ '--api-key', 'ccs-custom-key-123', '--regenerate-secret', ]); - - const config = loadUnifiedConfig(); - const managementSecret = config?.cliproxy.auth?.management_secret; - const configYamlPath = getConfigYamlPath(); + const configYamlPath = path.join(tempHome, '.ccs', 'config.yaml'); const diagnostics = { - exitCode, + exitCode: payload.exitCode, configYamlPath, configExists: fs.existsSync(configYamlPath), - apiKey: config?.cliproxy.auth?.api_key ?? null, - managementSecretLength: (managementSecret ?? '').length, + apiKey: payload.apiKey, + managementSecretLength: payload.managementSecretLength, }; if ( - exitCode !== 0 || - config?.cliproxy.auth?.api_key !== 'ccs-custom-key-123' || - typeof managementSecret !== 'string' || - (managementSecret ?? '').length <= 20 + payload.exitCode !== 0 || + payload.apiKey !== 'ccs-custom-key-123' || + payload.managementSecretLength <= 20 ) { throw new Error(`tokens rotation diagnostics: ${JSON.stringify(diagnostics)}`); } }); }); - it('rejects conflicting manual and generated secret flags', async () => { - await withScopedTokensHome(async () => { - const exitCode = await handleTokensCommand([ + it('rejects conflicting manual and generated secret flags', () => { + withScopedTokensHome((tempHome) => { + const { payload } = runTokensCommandInChild(tempHome, [ '--secret', 'manual-secret', '--regenerate-secret', ]); - expect(exitCode).toBe(1); - expect(fs.existsSync(getConfigYamlPath())).toBe(false); + expect(payload.exitCode).toBe(1); + expect(fs.existsSync(path.join(tempHome, '.ccs', 'config.yaml'))).toBe(false); }); }); });