Files
hermes-web-ui/tests/server/chat-run-baseline.test.ts
Zhicheng Han 1d54f36b2b test: add group chat phase 01 baselines (#1936)
Add observable-behavior baseline coverage for Group Chat sockets, REST routes, mention routing, chat-run, coding-agent dispatch, and client store state.

Keep the PR source diff focused on code/tests only; architecture context belongs in the PR body.
2026-07-05 11:14:11 +08:00

131 lines
4.4 KiB
TypeScript

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<typeof vi.fn>
disconnect: ReturnType<typeof vi.fn>
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<string, unknown>) {
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',
])
})
})