import { EventEmitter } from 'events' import { beforeEach, describe, expect, it, vi } from 'vitest' const ioMock = vi.hoisted(() => vi.fn()) const scenarioMock = vi.hoisted(() => ({ emitRunEvents: vi.fn(), })) vi.mock('socket.io-client', () => ({ io: ioMock, })) function makeSocket() { const emitter = new EventEmitter() as EventEmitter & { emit: ReturnType disconnect: ReturnType emitNative: (event: string, payload?: unknown) => boolean } const nativeEmit = EventEmitter.prototype.emit.bind(emitter) emitter.emitNative = nativeEmit emitter.emit = vi.fn((event: string, payload?: unknown) => { if (event === 'run') { process.nextTick(() => scenarioMock.emitRunEvents(nativeEmit, payload)) } return true }) as any emitter.disconnect = vi.fn() return emitter } function makeCtx(body: Record) { return { get: vi.fn((name: string) => name.toLowerCase() === 'authorization' ? 'Bearer token-1' : ''), state: { profile: { name: 'default' } }, request: { body }, status: 200, body: undefined as any, } } describe('chat-run action/event baseline', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns requires_action when approval is requested', async () => { const socket = makeSocket() ioMock.mockReturnValue(socket) scenarioMock.emitRunEvents.mockImplementation((emitNative: Function) => { emitNative('run.started', { run_id: 'run-1' }) emitNative('approval.requested', { run_id: 'run-1', approval_id: 'approval-1', command: 'touch file' }) }) const { runOnce } = await import('../../packages/server/src/controllers/chat-run') const ctx = makeCtx({ session_id: 'session-1', input: 'needs approval', include_events: true }) const pending = runOnce(ctx as any) socket.emitNative('connect') await pending expect(ctx.status).toBe(409) expect(ctx.body).toMatchObject({ ok: false, status: 'requires_action', event: 'approval.requested', session_id: 'session-1', run_id: 'run-1', action: { event: 'approval.requested', approval_id: 'approval-1' }, }) expect(ctx.body.events.map((event: any) => event.event)).toEqual(['run.started', 'approval.requested']) }) it('returns requires_action when clarification is requested', async () => { const socket = makeSocket() ioMock.mockReturnValue(socket) scenarioMock.emitRunEvents.mockImplementation((emitNative: Function) => { emitNative('run.started', { run_id: 'run-1' }) emitNative('clarify.requested', { run_id: 'run-1', question: 'Which room?' }) }) const { runOnce } = await import('../../packages/server/src/controllers/chat-run') const ctx = makeCtx({ session_id: 'session-1', input: 'needs clarify', include_events: true }) const pending = runOnce(ctx as any) socket.emitNative('connect') await pending expect(ctx.status).toBe(409) expect(ctx.body).toMatchObject({ ok: false, status: 'requires_action', event: 'clarify.requested', action: { event: 'clarify.requested', question: 'Which room?' }, }) }) it('records bounded event history and accumulates output/reasoning when include_events is true', async () => { const socket = makeSocket() ioMock.mockReturnValue(socket) scenarioMock.emitRunEvents.mockImplementation((emitNative: Function) => { emitNative('run.started', { run_id: 'run-1' }) emitNative('reasoning.delta', { run_id: 'run-1', delta: 'thought' }) emitNative('tool.started', { run_id: 'run-1', name: 'lookup' }) emitNative('tool.completed', { run_id: 'run-1', name: 'lookup' }) emitNative('message.delta', { run_id: 'run-1', delta: 'hello' }) emitNative('run.completed', { run_id: 'run-1' }) }) const { runOnce } = await import('../../packages/server/src/controllers/chat-run') const ctx = makeCtx({ session_id: 'session-1', input: 'hello', include_events: true }) const pending = runOnce(ctx as any) socket.emitNative('connect') await pending expect(ctx.status).toBe(200) expect(ctx.body).toMatchObject({ ok: true, status: 'completed', output: 'hello', reasoning: 'thought', run_id: 'run-1', }) expect(ctx.body.events.map((event: any) => event.event)).toEqual([ 'run.started', 'reasoning.delta', 'tool.started', 'tool.completed', 'message.delta', 'run.completed', ]) }) })