import { afterEach, describe, expect, it, vi } from 'vitest'; import { MessageQueue2 } from '@/utils/MessageQueue2'; import type { EnhancedMode } from './loop'; const harness = vi.hoisted(() => ({ notifications: [] as Array<{ method: string; params: unknown }>, registerRequestCalls: [] as string[], initializeCalls: [] as unknown[] })); vi.mock('./codexAppServerClient', () => { class MockCodexAppServerClient { private notificationHandler: ((method: string, params: unknown) => void) | null = null; async connect(): Promise {} async initialize(params: unknown): Promise<{ protocolVersion: number }> { harness.initializeCalls.push(params); return { protocolVersion: 1 }; } setNotificationHandler(handler: ((method: string, params: unknown) => void) | null): void { this.notificationHandler = handler; } registerRequestHandler(method: string): void { harness.registerRequestCalls.push(method); } async startThread(): Promise<{ thread: { id: string }; model: string }> { return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' }; } async resumeThread(): Promise<{ thread: { id: string }; model: string }> { return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' }; } async startTurn(): Promise<{ turn: Record }> { const started = { turn: {} }; harness.notifications.push({ method: 'turn/started', params: started }); this.notificationHandler?.('turn/started', started); const completed = { status: 'Completed', turn: {} }; harness.notifications.push({ method: 'turn/completed', params: completed }); this.notificationHandler?.('turn/completed', completed); return { turn: {} }; } async interruptTurn(): Promise> { return {}; } async disconnect(): Promise {} } return { CodexAppServerClient: MockCodexAppServerClient }; }); vi.mock('./utils/buildHapiMcpBridge', () => ({ buildHapiMcpBridge: async () => ({ server: { stop: () => {} }, mcpServers: {} }) })); import { codexRemoteLauncher } from './codexRemoteLauncher'; type FakeAgentState = { requests: Record; completedRequests: Record; }; function createMode(): EnhancedMode { return { permissionMode: 'default', collaborationMode: 'default' }; } function createSessionStub() { const queue = new MessageQueue2((mode) => JSON.stringify(mode)); queue.push('hello from launcher test', createMode()); queue.close(); const sessionEvents: Array<{ type: string; [key: string]: unknown }> = []; const codexMessages: unknown[] = []; const thinkingChanges: boolean[] = []; const foundSessionIds: string[] = []; let currentModel: string | null | undefined; let agentState: FakeAgentState = { requests: {}, completedRequests: {} }; const rpcHandlers = new Map unknown>(); const client = { rpcHandlerManager: { registerHandler(method: string, handler: (params: unknown) => unknown) { rpcHandlers.set(method, handler); } }, updateAgentState(handler: (state: FakeAgentState) => FakeAgentState) { agentState = handler(agentState); }, sendAgentMessage(message: unknown) { codexMessages.push(message); }, sendUserMessage(_text: string) {}, sendSessionEvent(event: { type: string; [key: string]: unknown }) { sessionEvents.push(event); } }; const session = { path: '/tmp/hapi-update', logPath: '/tmp/hapi-update/test.log', client, queue, codexArgs: undefined, codexCliOverrides: undefined, sessionId: null as string | null, thinking: false, getPermissionMode() { return 'default' as const; }, setModel(nextModel: string | null) { currentModel = nextModel; }, getModel() { return currentModel; }, onThinkingChange(nextThinking: boolean) { session.thinking = nextThinking; thinkingChanges.push(nextThinking); }, onSessionFound(id: string) { session.sessionId = id; foundSessionIds.push(id); }, sendAgentMessage(message: unknown) { client.sendAgentMessage(message); }, sendSessionEvent(event: { type: string; [key: string]: unknown }) { client.sendSessionEvent(event); }, sendUserMessage(text: string) { client.sendUserMessage(text); } }; return { session, sessionEvents, codexMessages, thinkingChanges, foundSessionIds, rpcHandlers, getModel: () => currentModel, getAgentState: () => agentState }; } describe('codexRemoteLauncher', () => { afterEach(() => { harness.notifications = []; harness.registerRequestCalls = []; harness.initializeCalls = []; }); it('finishes a turn and emits ready when task lifecycle events omit turn_id', async () => { const { session, sessionEvents, thinkingChanges, foundSessionIds, getModel } = createSessionStub(); const exitReason = await codexRemoteLauncher(session as never); expect(exitReason).toBe('exit'); expect(foundSessionIds).toContain('thread-anonymous'); expect(getModel()).toBe('gpt-5.4'); expect(harness.initializeCalls).toEqual([{ clientInfo: { name: 'hapi-codex-client', version: '1.0.0' }, capabilities: { experimentalApi: true } }]); expect(harness.notifications.map((entry) => entry.method)).toEqual(['turn/started', 'turn/completed']); expect(sessionEvents.filter((event) => event.type === 'ready').length).toBeGreaterThanOrEqual(1); expect(thinkingChanges).toContain(true); expect(session.thinking).toBe(false); }); });