refactor(codex): extract session management and launcher logic into separate modules

Reorganized runCodex.ts to improve maintainability by extracting:
- CodexSession class for session lifecycle management
- CodexLocalLauncher and CodexRemoteLauncher for mode-specific initialization
- CodexEventConverter for MCP message handling and UI buffer updates
- CodexSessionScanner for resume file discovery
- emitReadyIfIdle utility for ready event emission

Added codexSessionId field to metadata schema for session tracking.
Updated UI components to work with refactored architecture.
This commit is contained in:
weishu
2025-12-22 19:14:47 +08:00
parent 0c16b7b215
commit 4290f539bd
15 changed files with 1606 additions and 661 deletions
@@ -0,0 +1,76 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdir, writeFile, appendFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { existsSync } from 'node:fs';
import { createCodexSessionScanner } from './codexSessionScanner';
import type { CodexSessionEvent } from './codexEventConverter';
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe('codexSessionScanner', () => {
let testDir: string;
let sessionsDir: string;
let sessionFile: string;
let originalCodexHome: string | undefined;
let scanner: Awaited<ReturnType<typeof createCodexSessionScanner>> | null = null;
let events: CodexSessionEvent[] = [];
beforeEach(async () => {
testDir = join(tmpdir(), `codex-scanner-${Date.now()}`);
sessionsDir = join(testDir, 'sessions', '2025', '12', '22');
await mkdir(sessionsDir, { recursive: true });
originalCodexHome = process.env.CODEX_HOME;
process.env.CODEX_HOME = testDir;
events = [];
});
afterEach(async () => {
if (scanner) {
await scanner.cleanup();
scanner = null;
}
if (originalCodexHome === undefined) {
delete process.env.CODEX_HOME;
} else {
process.env.CODEX_HOME = originalCodexHome;
}
if (existsSync(testDir)) {
await rm(testDir, { recursive: true, force: true });
}
});
it('emits only new events after startup', async () => {
const sessionId = 'session-123';
sessionFile = join(sessionsDir, `codex-${sessionId}.jsonl`);
const initialLines = [
JSON.stringify({ type: 'session_meta', payload: { id: sessionId } }),
JSON.stringify({ type: 'event_msg', payload: { type: 'agent_message', message: 'hello' } })
];
await writeFile(sessionFile, initialLines.join('\n') + '\n');
scanner = await createCodexSessionScanner({
sessionId,
onEvent: (event) => events.push(event)
});
await wait(150);
expect(events).toHaveLength(0);
const newLine = JSON.stringify({
type: 'response_item',
payload: { type: 'function_call', name: 'Tool', call_id: 'call-1', arguments: '{}' }
});
await appendFile(sessionFile, newLine + '\n');
await wait(200);
expect(events).toHaveLength(1);
expect(events[0].type).toBe('response_item');
});
});