/** * Unit tests for codex-auth import-default command. * * Covers: * - missing legacy auth.json → clean error * - profile exists no --force → refuses with hint * - profile exists --force → backup created, overwrite * - cliproxy-format source → rejects with clear message * - torn-write retry: truncated JSON twice then full → succeeds on 3rd read * - persistent torn state → clean error, not silent corruption * - pgrep mock returning PID → warns + refuses without --force-while-running * - --force-while-running bypasses pgrep check * - --with-history copies history.jsonl + sessions/ * - --with-history default false → not copied * - atomic write: tmp file gone after rename */ import { afterEach, beforeEach, describe, expect, it, spyOn, mock } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as childProcess from 'child_process'; // Note: spyOn(fs, 'readFileSync') crashes Bun's process due to native module binding. // Torn-write retry tests use real file replacement via timer instead. // Build a minimal valid JWT with given payload for test fixtures function makeJwt(payload: Record): string { const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url'); const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); return `${header}.${body}.fakesig`; } const VALID_JWT = makeJwt({ email: 'test@example.com', 'https://api.openai.com/auth': { chatgpt_plan_type: 'plus', chatgpt_account_id: 'acct-123', }, }); const VALID_AUTH_JSON = JSON.stringify({ tokens: { id_token: VALID_JWT } }); let tempDir: string; let ccsHome: string; let legacyCodexHome: string; const ORIG_CCS_HOME = process.env.CCS_HOME; const ORIG_LEGACY_CODEX_HOME = process.env.LEGACY_CODEX_HOME; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-import-default-test-')); ccsHome = path.join(tempDir, 'ccs'); legacyCodexHome = path.join(tempDir, 'legacy-codex'); fs.mkdirSync(path.join(ccsHome, '.ccs'), { recursive: true }); fs.mkdirSync(legacyCodexHome, { recursive: true }); process.env.CCS_HOME = ccsHome; process.env.LEGACY_CODEX_HOME = legacyCodexHome; // Default: pgrep finds nothing (no Codex running). Tests that need a positive // result override this per-test. Without this default, `pgrep -f codex` on a // dev machine matches the Claude Code process itself and exits early with code 7. spyOn(childProcess, 'spawnSync').mockReturnValue({ status: 1, stdout: '', stderr: '', pid: 0, output: [], signal: null, error: undefined, }); }); afterEach(() => { if (ORIG_CCS_HOME === undefined) delete process.env.CCS_HOME; else process.env.CCS_HOME = ORIG_CCS_HOME; if (ORIG_LEGACY_CODEX_HOME === undefined) delete process.env.LEGACY_CODEX_HOME; else process.env.LEGACY_CODEX_HOME = ORIG_LEGACY_CODEX_HOME; fs.rmSync(tempDir, { recursive: true, force: true }); mock.restore(); }); async function makeCtx() { const { CodexProfileRegistry } = await import( '../../../../src/codex-auth/codex-profile-registry' ); return { registry: new CodexProfileRegistry(), version: '0.0.0-test', }; } function silenceConsole(): () => void { const origLog = console.log; const origErr = console.error; const origWarn = console.warn; const origStdErr = process.stderr.write.bind(process.stderr); console.log = () => {}; console.error = () => {}; console.warn = () => {}; process.stderr.write = () => true; return () => { console.log = origLog; console.error = origErr; console.warn = origWarn; process.stderr.write = origStdErr; }; } function captureOutput(): { stderr: string[]; restore: () => void } { const stderr: string[] = []; const origStdErr = process.stderr.write.bind(process.stderr); const origLog = console.log; const origErr = console.error; console.log = () => {}; console.error = (...args: unknown[]) => { stderr.push(args.join(' ')); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any process.stderr.write = (chunk: any) => { stderr.push(String(chunk)); return true; }; return { stderr, restore: () => { console.log = origLog; console.error = origErr; process.stderr.write = origStdErr; }, }; } function mockProcessTable(pgrepStdout: string, psStdout: string) { spyOn(childProcess, 'spawnSync').mockImplementation( // eslint-disable-next-line @typescript-eslint/no-explicit-any (cmd: string, _args: string[]): any => { if (cmd === 'pgrep') { return { status: pgrepStdout.trim().length > 0 ? 0 : 1, stdout: pgrepStdout, stderr: '', pid: 0, output: [], signal: null, error: undefined, }; } if (cmd === 'ps') { return { status: psStdout.trim().length > 0 ? 0 : 1, stdout: psStdout, stderr: '', pid: 0, output: [], signal: null, error: undefined, }; } return { status: 1, stdout: '', stderr: '', pid: 0, output: [], signal: null, error: undefined, }; } ); } // ───────────────────────────────────────────────────────────────────────────── describe('import-default — missing legacy auth.json', () => { it('exits with clear error when ~/.codex/auth.json does not exist', async () => { const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['myprofile']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('myprofile')).toBe(false); }); }); describe('import-default — option validation', () => { it('rejects unsupported flags before importing legacy auth', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCount = 0; const origExit = process.exit; process.exit = () => { exitCount++; throw new Error('exit'); }; const captured = captureOutput(); try { await handleImportDefaultCodex(ctx, ['typo', '--with-historyy']); } catch { /* expected */ } try { await handleImportDefaultCodex(ctx, ['shellleak', '--shell', 'fish']); } catch { /* expected */ } try { await handleImportDefaultCodex(ctx, ['jsonleak', '--json']); } catch { /* expected */ } try { await handleImportDefaultCodex(ctx, ['yesleak', '--yes']); } catch { /* expected */ } finally { captured.restore(); process.exit = origExit; } expect(exitCount).toBe(4); expect(ctx.registry.hasProfile('typo')).toBe(false); expect(ctx.registry.hasProfile('shellleak')).toBe(false); expect(ctx.registry.hasProfile('jsonleak')).toBe(false); expect(ctx.registry.hasProfile('yesleak')).toBe(false); expect(captured.stderr.join('')).toContain('Usage:'); expect(captured.stderr.join('')).toContain('--shell'); }); }); describe('import-default — profile collision without --force', () => { it('refuses when profile exists without --force', async () => { // Write valid legacy auth fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); // Pre-create the profile ctx.registry.createProfile('myprofile'); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['myprofile']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); }); }); describe('import-default — --force overwrites and creates backup', () => { it('creates .bak file and overwrites auth.json when --force passed', async () => { // Write valid legacy auth fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); // First import (no --force needed since profile doesn't exist) const restore1 = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['backuptest']); } finally { restore1(); } // Verify profile was created const profileDir = path.join(ccsHome, '.ccs', 'codex-instances', 'backuptest'); const destAuth = path.join(profileDir, 'auth.json'); expect(fs.existsSync(destAuth)).toBe(true); // Update legacy with different data const newJwt = makeJwt({ email: 'new@example.com' }); fs.writeFileSync( path.join(legacyCodexHome, 'auth.json'), JSON.stringify({ tokens: { id_token: newJwt } }) ); // Re-run with --force const restore2 = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['backuptest', '--force']); } finally { restore2(); } // Backup file should exist const files = fs.readdirSync(profileDir); const bakFile = files.find((f) => f.startsWith('auth.json.bak-')); expect(bakFile).toBeDefined(); // New auth.json should contain the new JWT (which encodes new@example.com) // Verify by checking registry metadata which decodes the JWT const meta = ctx.registry.getProfile('backuptest'); expect(meta.email).toBe('new@example.com'); }); }); describe('import-default — cliproxy-format rejection', () => { it('rejects auth files with type field (CLIProxy wrapper format)', async () => { const cliproxyAuth = JSON.stringify({ type: 'codex', account_id: 'abc', tokens: { id_token: VALID_JWT }, }); fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), cliproxyAuth); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['cliptest']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('cliptest')).toBe(false); }); }); describe('import-default — torn-write retry', () => { it('retries on truncated JSON and succeeds after file is fixed', async () => { const authPath = path.join(legacyCodexHome, 'auth.json'); // Write truncated JSON initially — simulates torn write mid-file fs.writeFileSync(authPath, '{truncated'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); // After 50ms (before 2nd retry at 100ms) replace with valid JSON const fixTimer = setTimeout(() => { fs.writeFileSync(authPath, VALID_AUTH_JSON); }, 50); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['retrytest']); } finally { restore(); clearTimeout(fixTimer); } // The retry succeeded once file was repaired expect(ctx.registry.hasProfile('retrytest')).toBe(true); }); it('fails cleanly on persistent torn state (all retries fail)', async () => { const authPath = path.join(legacyCodexHome, 'auth.json'); // Write persistently invalid JSON — all retries will fail fs.writeFileSync(authPath, '{always-truncated'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['torntest']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('torntest')).toBe(false); }); it('rejects a malformed 3-segment id_token payload', async () => { const authPath = path.join(legacyCodexHome, 'auth.json'); fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: 'header.not-json.sig' } })); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['badjwt']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('badjwt')).toBe(false); }); it('rejects an id_token payload with invalid base64url characters', async () => { const authPath = path.join(legacyCodexHome, 'auth.json'); fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: 'h.e30$.s' } })); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['bad-base64url']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('bad-base64url')).toBe(false); }); it('rejects an id_token signature with impossible base64url length', async () => { const authPath = path.join(legacyCodexHome, 'auth.json'); const [header, payload] = VALID_JWT.split('.'); fs.writeFileSync(authPath, JSON.stringify({ tokens: { id_token: `${header}.${payload}.a` } })); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['bad-signature']); } catch { /* expected */ } finally { restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(ctx.registry.hasProfile('bad-signature')).toBe(false); }); }); describe('import-default — Codex running detection', () => { it('warns and refuses when pgrep finds a codex PID', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); mockProcessTable('12345\n', '12345 /usr/local/bin/codex login\n'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const captured = captureOutput(); try { await handleImportDefaultCodex(ctx, ['runningtest']); } catch { /* expected */ } finally { captured.restore(); process.exit = origExit; } expect(exitCalled).toBe(true); const stderrMsg = captured.stderr.join(''); expect(stderrMsg).toContain('12345'); }); it('proceeds with --force-while-running even when Codex is running', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); mockProcessTable('12345\n', '12345 /usr/local/bin/codex login\n'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['forcerunning', '--force-while-running']); } finally { restore(); } // Should have proceeded and created the profile expect(ctx.registry.hasProfile('forcerunning')).toBe(true); }); it('warns and refuses when Codex is running through a node shim', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); mockProcessTable('12345\n', '12345 /usr/bin/node /usr/local/bin/codex login\n'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); let exitCalled = false; const origExit = process.exit; process.exit = () => { exitCalled = true; throw new Error('exit'); }; const captured = captureOutput(); try { await handleImportDefaultCodex(ctx, ['nodeshim']); } catch { /* expected */ } finally { captured.restore(); process.exit = origExit; } expect(exitCalled).toBe(true); expect(captured.stderr.join('')).toContain('12345'); expect(ctx.registry.hasProfile('nodeshim')).toBe(false); }); it('ignores pgrep false positives that are not Codex executables', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); mockProcessTable('11111\n', '11111 /usr/bin/node /tmp/codex-auth-helper.js\n'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['falsepositive']); } finally { restore(); } expect(ctx.registry.hasProfile('falsepositive')).toBe(true); }); }); describe('import-default — --with-history', () => { it('copies history.jsonl and sessions/ when --with-history passed', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); fs.writeFileSync(path.join(legacyCodexHome, 'history.jsonl'), '{"prompt":"hello"}\n'); const sessionsDir = path.join(legacyCodexHome, 'sessions'); fs.mkdirSync(sessionsDir, { recursive: true }); fs.writeFileSync(path.join(sessionsDir, 'sess1.json'), '{}'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['withhistory', '--with-history']); } finally { restore(); } const profileDir = path.join(ccsHome, '.ccs', 'codex-instances', 'withhistory'); expect(fs.existsSync(path.join(profileDir, 'history.jsonl'))).toBe(true); expect(fs.existsSync(path.join(profileDir, 'sessions', 'sess1.json'))).toBe(true); }); it('does NOT copy history.jsonl by default (D8: false default)', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); fs.writeFileSync(path.join(legacyCodexHome, 'history.jsonl'), '{"prompt":"hello"}\n'); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['nohistory']); } finally { restore(); } const profileDir = path.join(ccsHome, '.ccs', 'codex-instances', 'nohistory'); expect(fs.existsSync(path.join(profileDir, 'history.jsonl'))).toBe(false); }); }); describe('import-default — atomic write', () => { it('leaves no tmp file after successful import', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['atomictest']); } finally { restore(); } const profileDir = path.join(ccsHome, '.ccs', 'codex-instances', 'atomictest'); const files = fs.readdirSync(profileDir); const tmpFiles = files.filter((f) => f.includes('.tmp.')); expect(tmpFiles.length).toBe(0); }); }); describe('import-default — happy path end-to-end', () => { it('registers profile with decoded email in registry', async () => { fs.writeFileSync(path.join(legacyCodexHome, 'auth.json'), VALID_AUTH_JSON); const { handleImportDefaultCodex } = await import( '../../../../src/codex-auth/commands/import-default-command' ); const ctx = await makeCtx(); const restore = silenceConsole(); try { await handleImportDefaultCodex(ctx, ['happypath']); } finally { restore(); } expect(ctx.registry.hasProfile('happypath')).toBe(true); const meta = ctx.registry.getProfile('happypath'); expect(meta.email).toBe('test@example.com'); expect(meta.plan_type).toBe('plus'); }); });