mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 21:40:13 +00:00
d0f1e7d1f2
* feat(bridge): refactor compression to use DB history and add structured logging - Extract buildDbHistory() to share message loading between buildCompressedHistory and forceCompressBridgeHistory - forceCompressBridgeHistory now reads from local DB instead of using Python-provided messages, ensuring consistency with api_server path - Pass sessionId to compressor for snapshot-aware compression - Add force_compress flag to bridge chat requests - Add bridgeLogger structured logging for compression lifecycle - Simplify schemas, session-sync, and providers Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix bridge compression history handling --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const getCompressionSnapshotMock = vi.fn()
|
|
const saveCompressionSnapshotMock = vi.fn()
|
|
const deleteCompressionSnapshotMock = vi.fn()
|
|
|
|
vi.mock('../../packages/server/src/services/logger', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/compression-snapshot', () => ({
|
|
getCompressionSnapshot: getCompressionSnapshotMock,
|
|
saveCompressionSnapshot: saveCompressionSnapshotMock,
|
|
deleteCompressionSnapshot: deleteCompressionSnapshotMock,
|
|
}))
|
|
|
|
describe('ChatContextCompressor', () => {
|
|
let originalFetch: typeof global.fetch
|
|
|
|
beforeEach(() => {
|
|
originalFetch = global.fetch
|
|
getCompressionSnapshotMock.mockReset()
|
|
saveCompressionSnapshotMock.mockReset()
|
|
deleteCompressionSnapshotMock.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch
|
|
})
|
|
|
|
it('keeps full history when full summarization fails', async () => {
|
|
const { ChatContextCompressor } = await import('../../packages/server/src/lib/context-compressor')
|
|
const compressor = new ChatContextCompressor({ config: { tailMessageCount: 3 } })
|
|
const messages = Array.from({ length: 8 }, (_, i) => ({
|
|
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
content: `message ${i}`,
|
|
}))
|
|
|
|
getCompressionSnapshotMock.mockReturnValue(null)
|
|
global.fetch = vi.fn(async () => ({ ok: false, status: 500 })) as any
|
|
|
|
const result = await compressor.compress(messages, 'http://upstream', undefined, 's1')
|
|
|
|
expect(result.messages).toHaveLength(messages.length)
|
|
expect(result.messages.map(m => m.content)).toEqual(messages.map(m => m.content))
|
|
expect(result.meta.compressed).toBe(false)
|
|
expect(result.meta.llmCompressed).toBe(false)
|
|
expect(saveCompressionSnapshotMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps all new messages when incremental summarization fails', async () => {
|
|
const { ChatContextCompressor, SUMMARY_PREFIX } = await import('../../packages/server/src/lib/context-compressor')
|
|
const compressor = new ChatContextCompressor({ config: { tailMessageCount: 3 } })
|
|
const messages = Array.from({ length: 8 }, (_, i) => ({
|
|
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
content: `message ${i}`,
|
|
}))
|
|
|
|
getCompressionSnapshotMock.mockReturnValue({
|
|
summary: 'previous summary',
|
|
lastMessageIndex: 1,
|
|
messageCountAtTime: 2,
|
|
})
|
|
global.fetch = vi.fn(async () => ({ ok: false, status: 500 })) as any
|
|
|
|
const result = await compressor.compress(messages, 'http://upstream', undefined, 's1')
|
|
|
|
expect(result.messages).toHaveLength(7)
|
|
expect(result.messages[0]).toEqual({
|
|
role: 'user',
|
|
content: `${SUMMARY_PREFIX}\n\nprevious summary`,
|
|
})
|
|
expect(result.messages.slice(1).map(m => m.content)).toEqual(messages.slice(2).map(m => m.content))
|
|
expect(result.meta.compressed).toBe(true)
|
|
expect(result.meta.llmCompressed).toBe(false)
|
|
expect(result.meta.compressedStartIndex).toBe(1)
|
|
expect(result.meta.verbatimCount).toBe(6)
|
|
expect(saveCompressionSnapshotMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not call the summarizer when snapshot has only tail messages after it', async () => {
|
|
const { ChatContextCompressor, SUMMARY_PREFIX } = await import('../../packages/server/src/lib/context-compressor')
|
|
const compressor = new ChatContextCompressor({ config: { tailMessageCount: 10 } })
|
|
const messages = Array.from({ length: 6 }, (_, i) => ({
|
|
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
content: `message ${i}`,
|
|
}))
|
|
const fetchMock = vi.fn()
|
|
|
|
getCompressionSnapshotMock.mockReturnValue({
|
|
summary: 'previous summary',
|
|
lastMessageIndex: 3,
|
|
messageCountAtTime: 4,
|
|
})
|
|
global.fetch = fetchMock as any
|
|
|
|
const result = await compressor.compress(messages, 'http://upstream', undefined, 's1')
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
expect(result.messages).toHaveLength(3)
|
|
expect(result.messages[0].content).toBe(`${SUMMARY_PREFIX}\n\nprevious summary`)
|
|
expect(result.messages.slice(1).map(m => m.content)).toEqual(['message 4', 'message 5'])
|
|
expect(result.meta.llmCompressed).toBe(false)
|
|
expect(result.meta.compressedStartIndex).toBe(3)
|
|
expect(saveCompressionSnapshotMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|