mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 13:30:14 +00:00
3f88553765
* feat: add single-page live session monitor and chat pinning * fix: restore full test green after main merge * fix: use Array.from instead of Set spread for ts-node compatibility [...new Set()] requires downlevelIteration which isn't enabled in ts-node dev mode, causing sonic-boom crash on startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: ekko <fqsy1416@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
8.3 KiB
TypeScript
264 lines
8.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const exportSessionsRawMock = vi.fn()
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({
|
|
exportSessionsRaw: exportSessionsRawMock,
|
|
}))
|
|
|
|
describe('conversations service', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2026-04-20T00:00:00Z'))
|
|
exportSessionsRawMock.mockReset()
|
|
})
|
|
|
|
it('aggregates a single compression continuation even when the child preview differs', async () => {
|
|
exportSessionsRawMock.mockResolvedValue([
|
|
{
|
|
id: 'root',
|
|
parent_session_id: null,
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: null,
|
|
started_at: 100,
|
|
ended_at: 110,
|
|
end_reason: 'compression',
|
|
message_count: 2,
|
|
tool_call_count: 0,
|
|
input_tokens: 5,
|
|
output_tokens: 8,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0.1,
|
|
actual_cost_usd: 0.1,
|
|
cost_status: 'estimated',
|
|
messages: [
|
|
{ id: 1, session_id: 'root', role: 'user', content: 'Start here', timestamp: 101 },
|
|
{ id: 2, session_id: 'root', role: 'assistant', content: 'Assistant reply', timestamp: 102 },
|
|
],
|
|
},
|
|
{
|
|
id: 'root-cont',
|
|
parent_session_id: 'root',
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: 'Continuation',
|
|
started_at: 110,
|
|
ended_at: 111,
|
|
end_reason: null,
|
|
message_count: 2,
|
|
tool_call_count: 0,
|
|
input_tokens: 3,
|
|
output_tokens: 4,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0.2,
|
|
actual_cost_usd: 0.2,
|
|
cost_status: 'final',
|
|
messages: [
|
|
{ id: 3, session_id: 'root-cont', role: 'user', content: 'Continue with more detail', timestamp: 110 },
|
|
{ id: 4, session_id: 'root-cont', role: 'assistant', content: 'Continued answer', timestamp: 111 },
|
|
],
|
|
},
|
|
])
|
|
|
|
const mod = await import('../../packages/server/src/services/hermes/conversations')
|
|
const summaries = await mod.listConversationSummaries({ humanOnly: true })
|
|
|
|
expect(summaries).toHaveLength(1)
|
|
expect(summaries[0]).toEqual(
|
|
expect.objectContaining({
|
|
id: 'root',
|
|
thread_session_count: 2,
|
|
ended_at: 111,
|
|
cost_status: 'mixed',
|
|
actual_cost_usd: 0.30000000000000004,
|
|
}),
|
|
)
|
|
|
|
const detail = await mod.getConversationDetail('root', { humanOnly: true })
|
|
expect(detail?.thread_session_count).toBe(2)
|
|
expect(detail?.messages.map((message: any) => message.content)).toEqual([
|
|
'Start here',
|
|
'Assistant reply',
|
|
'Continue with more detail',
|
|
'Continued answer',
|
|
])
|
|
})
|
|
|
|
it('treats branched children as their own visible conversations', async () => {
|
|
exportSessionsRawMock.mockResolvedValue([
|
|
{
|
|
id: 'root',
|
|
parent_session_id: null,
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: 'Root',
|
|
started_at: 100,
|
|
ended_at: 200,
|
|
end_reason: 'branched',
|
|
message_count: 1,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: 0,
|
|
cost_status: 'estimated',
|
|
messages: [{ id: 1, session_id: 'root', role: 'user', content: 'Root prompt', timestamp: 101 }],
|
|
},
|
|
{
|
|
id: 'branch-child',
|
|
parent_session_id: 'root',
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: 'Branch child',
|
|
started_at: 201,
|
|
ended_at: 210,
|
|
end_reason: null,
|
|
message_count: 2,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: 0,
|
|
cost_status: 'estimated',
|
|
messages: [
|
|
{ id: 2, session_id: 'branch-child', role: 'user', content: 'Branch prompt', timestamp: 202 },
|
|
{ id: 3, session_id: 'branch-child', role: 'assistant', content: 'Branch answer', timestamp: 203 },
|
|
],
|
|
},
|
|
])
|
|
|
|
const mod = await import('../../packages/server/src/services/hermes/conversations')
|
|
const summaries = await mod.listConversationSummaries({ humanOnly: true })
|
|
|
|
expect(summaries.map((summary: any) => summary.id)).toEqual(['branch-child', 'root'])
|
|
|
|
const detail = await mod.getConversationDetail('branch-child', { humanOnly: true })
|
|
expect(detail?.messages.map((message: any) => message.content)).toEqual(['Branch prompt', 'Branch answer'])
|
|
})
|
|
|
|
it('excludes human-only conversations with no visible human messages', async () => {
|
|
exportSessionsRawMock.mockResolvedValue([
|
|
{
|
|
id: 'synthetic-root',
|
|
parent_session_id: null,
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: null,
|
|
started_at: 100,
|
|
ended_at: 101,
|
|
end_reason: null,
|
|
message_count: 1,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: 0,
|
|
cost_status: 'estimated',
|
|
messages: [
|
|
{
|
|
id: 1,
|
|
session_id: 'synthetic-root',
|
|
role: 'user',
|
|
content: "You've reached the maximum number of tool-calling iterations allowed.",
|
|
timestamp: 100,
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
const mod = await import('../../packages/server/src/services/hermes/conversations')
|
|
const summaries = await mod.listConversationSummaries({ humanOnly: true })
|
|
const detail = await mod.getConversationDetail('synthetic-root', { humanOnly: true })
|
|
|
|
expect(summaries).toEqual([])
|
|
expect(detail).toBeNull()
|
|
})
|
|
|
|
it('caches raw exports briefly and normalizes structured message content', async () => {
|
|
exportSessionsRawMock.mockResolvedValue([
|
|
{
|
|
id: 'recent-open',
|
|
parent_session_id: null,
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: 'Recent open',
|
|
started_at: 1776643190,
|
|
ended_at: null,
|
|
end_reason: null,
|
|
message_count: 1,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: 0,
|
|
cost_status: 'estimated',
|
|
messages: [
|
|
{
|
|
id: 11,
|
|
session_id: 'recent-open',
|
|
role: 'assistant',
|
|
content: [{ text: 'hello' }, { text: 'world' }],
|
|
timestamp: 1776643198,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'stale-open',
|
|
parent_session_id: null,
|
|
source: 'cli',
|
|
model: 'openai/gpt-5.4',
|
|
title: 'Stale open',
|
|
started_at: 1776642000,
|
|
ended_at: null,
|
|
end_reason: null,
|
|
message_count: 0,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: 'openai',
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: 0,
|
|
cost_status: 'estimated',
|
|
messages: [],
|
|
},
|
|
])
|
|
|
|
const mod = await import('../../packages/server/src/services/hermes/conversations')
|
|
const firstSummaries = await mod.listConversationSummaries({ humanOnly: false })
|
|
const detail = await mod.getConversationDetail('recent-open', { humanOnly: false })
|
|
const secondSummaries = await mod.listConversationSummaries({ humanOnly: false })
|
|
|
|
expect(exportSessionsRawMock).toHaveBeenCalledTimes(1)
|
|
expect(firstSummaries.find((summary: any) => summary.id === 'recent-open')?.is_active).toBe(true)
|
|
expect(secondSummaries.find((summary: any) => summary.id === 'stale-open')?.is_active).toBe(false)
|
|
expect(detail?.messages[0].content).toBe('hello\nworld')
|
|
})
|
|
})
|