Files
hermes-web-ui/tests/server/response-stream-reasoning-storage.test.ts

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.',
})
})
})