mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-11 11:10:21 +00:00
1d54f36b2b
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.
131 lines
4.4 KiB
TypeScript
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',
|
|
])
|
|
})
|
|
})
|