mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-13 20:20:46 +00:00
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import { applyResponseStreamEvent, flushResponseRunToDb } from '../../packages/server/src/services/hermes/run-chat/response-stream'
|
|
import type { SessionState } from '../../packages/server/src/services/hermes/run-chat/types'
|
|
|
|
const { addMessageMock } = vi.hoisted(() => ({
|
|
addMessageMock: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
|
|
addMessage: addMessageMock,
|
|
}))
|
|
|
|
describe('response stream reasoning storage', () => {
|
|
beforeEach(() => {
|
|
addMessageMock.mockReset()
|
|
})
|
|
|
|
it('buffers reasoning without creating an empty assistant message before text arrives', () => {
|
|
const state: SessionState = { messages: [], isWorking: false, events: [], queue: [] }
|
|
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.created', {
|
|
response: { id: 'resp-1', status: 'in_progress' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.reasoning.delta', {
|
|
delta: 'think first',
|
|
})
|
|
|
|
expect(state.messages).toEqual([])
|
|
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_text.delta', {
|
|
delta: 'answer',
|
|
})
|
|
|
|
expect(state.messages).toEqual([
|
|
expect.objectContaining({
|
|
role: 'assistant',
|
|
content: 'answer',
|
|
reasoning: 'think first',
|
|
reasoning_content: 'think first',
|
|
}),
|
|
])
|
|
})
|
|
|
|
it('keeps reasoning deltas in session memory across tool boundaries', () => {
|
|
const state: SessionState = { messages: [], isWorking: false, events: [], queue: [] }
|
|
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.created', {
|
|
response: { id: 'resp-1', status: 'in_progress' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.reasoning.delta', {
|
|
delta: 'think before. ',
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_text.delta', {
|
|
delta: 'Before tool.',
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_item.done', {
|
|
item: { type: 'function_call', call_id: 'tool-1', name: 'Bash', arguments: '{}' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.reasoning.delta', {
|
|
delta: 'think after. ',
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_item.done', {
|
|
item: { type: 'function_call_output', call_id: 'tool-1', output: 'tool output' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_text.delta', {
|
|
delta: 'After tool.',
|
|
})
|
|
|
|
expect(state.messages.map(message => message.role)).toEqual(['assistant', 'assistant', 'tool', 'assistant'])
|
|
expect(state.messages[0]).toMatchObject({
|
|
role: 'assistant',
|
|
content: 'Before tool.',
|
|
reasoning: 'think before. think after. ',
|
|
reasoning_content: 'think before. think after. ',
|
|
})
|
|
expect(state.messages[3]).toMatchObject({
|
|
role: 'assistant',
|
|
content: 'After tool.',
|
|
})
|
|
})
|
|
|
|
it('flushes reasoning fields to message storage', () => {
|
|
const state: SessionState = { messages: [], isWorking: false, events: [], queue: [] }
|
|
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.created', {
|
|
response: { id: 'resp-1', status: 'in_progress' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.reasoning.delta', {
|
|
delta: 'stored thinking',
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.output_text.delta', {
|
|
delta: 'answer',
|
|
})
|
|
|
|
flushResponseRunToDb(state, 'session-1')
|
|
|
|
expect(addMessageMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
session_id: 'session-1',
|
|
role: 'assistant',
|
|
content: 'answer',
|
|
reasoning: 'stored thinking',
|
|
reasoning_content: 'stored thinking',
|
|
}))
|
|
})
|
|
|
|
it('deduplicates final reasoning snapshots after streamed reasoning deltas', () => {
|
|
const state: SessionState = { messages: [], isWorking: false, events: [], queue: [] }
|
|
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.created', {
|
|
response: { id: 'resp-1', status: 'in_progress' },
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.reasoning.delta', {
|
|
delta: 'Need inspect.',
|
|
})
|
|
applyResponseStreamEvent(state, 'session-1', 'run-1', 'response.completed', {
|
|
response: {
|
|
id: 'resp-1',
|
|
output: [
|
|
{ type: 'reasoning', summary: [{ text: 'Need inspect.' }] },
|
|
{ type: 'message', content: [{ type: 'output_text', text: 'answer' }] },
|
|
],
|
|
},
|
|
})
|
|
|
|
expect(state.messages[0]).toMatchObject({
|
|
content: 'answer',
|
|
reasoning: 'Need inspect.',
|
|
reasoning_content: 'Need inspect.',
|
|
})
|
|
})
|
|
})
|