Files
vessel/frontend/src/lib/memory/summarizer.test.ts
vikingowl d81430e1aa test: extend test coverage for backend and frontend
Backend:
- Add fetcher_test.go (HTML stripping, URL fetching utilities)
- Add model_registry_test.go (parsing, size ranges, model matching)
- Add database_test.go (CRUD operations, migrations)
- Add tests for geolocation, search, tools, version handlers

Frontend unit tests (469 total):
- OllamaClient: 22 tests for API methods with mocked fetch
- Memory/RAG: tokenizer, chunker, summarizer, embeddings, vector-store
- Services: prompt-resolution, conversation-summary
- Components: Skeleton, BranchNavigator, ConfirmDialog, ThinkingBlock
- Utils: export, import, file-processor, keyboard
- Tools: builtin math parser (44 tests)

E2E tests (28 total):
- Set up Playwright with Chromium
- App loading, sidebar navigation, settings page
- Chat interface, responsive design, accessibility
- Import dialog, project modal interactions

Config changes:
- Add browser conditions to vitest.config.ts for Svelte 5 components
- Add playwright.config.ts for E2E testing
- Add test:e2e scripts to package.json
- Update .gitignore to exclude test artifacts

Closes #8
2026-01-22 11:05:49 +01:00

215 lines
6.5 KiB
TypeScript

/**
* Summarizer utility tests
*
* Tests the pure functions for conversation summarization
*/
import { describe, it, expect } from 'vitest';
import {
selectMessagesForSummarization,
calculateTokenSavings,
createSummaryRecord,
shouldSummarize,
formatSummaryAsContext
} from './summarizer';
import type { MessageNode } from '$lib/types/chat';
// Helper to create message nodes
function createMessageNode(
role: 'user' | 'assistant' | 'system',
content: string,
id?: string
): MessageNode {
return {
id: id || crypto.randomUUID(),
parentId: null,
siblingIds: [],
message: {
role,
content,
timestamp: Date.now()
}
};
}
describe('selectMessagesForSummarization', () => {
it('returns empty toSummarize when messages <= preserveCount', () => {
const messages = [
createMessageNode('user', 'Hi'),
createMessageNode('assistant', 'Hello'),
createMessageNode('user', 'How are you?'),
createMessageNode('assistant', 'Good')
];
const result = selectMessagesForSummarization(messages, 1000, 4);
expect(result.toSummarize).toHaveLength(0);
expect(result.toKeep).toHaveLength(4);
});
it('keeps recent messages and marks older for summarization', () => {
const messages = [
createMessageNode('user', 'Message 1'),
createMessageNode('assistant', 'Response 1'),
createMessageNode('user', 'Message 2'),
createMessageNode('assistant', 'Response 2'),
createMessageNode('user', 'Message 3'),
createMessageNode('assistant', 'Response 3'),
createMessageNode('user', 'Message 4'),
createMessageNode('assistant', 'Response 4')
];
const result = selectMessagesForSummarization(messages, 1000, 4);
expect(result.toSummarize).toHaveLength(4);
expect(result.toKeep).toHaveLength(4);
expect(result.toSummarize[0].message.content).toBe('Message 1');
expect(result.toKeep[0].message.content).toBe('Message 3');
});
it('preserves system messages in toKeep', () => {
const messages = [
createMessageNode('system', 'System prompt'),
createMessageNode('user', 'Message 1'),
createMessageNode('assistant', 'Response 1'),
createMessageNode('user', 'Message 2'),
createMessageNode('assistant', 'Response 2'),
createMessageNode('user', 'Message 3'),
createMessageNode('assistant', 'Response 3')
];
const result = selectMessagesForSummarization(messages, 1000, 4);
// System message should be in toKeep even though it's at the start
expect(result.toKeep.some((m) => m.message.role === 'system')).toBe(true);
expect(result.toSummarize.every((m) => m.message.role !== 'system')).toBe(true);
});
it('uses default preserveCount of 4', () => {
const messages = [
createMessageNode('user', 'M1'),
createMessageNode('assistant', 'R1'),
createMessageNode('user', 'M2'),
createMessageNode('assistant', 'R2'),
createMessageNode('user', 'M3'),
createMessageNode('assistant', 'R3'),
createMessageNode('user', 'M4'),
createMessageNode('assistant', 'R4')
];
const result = selectMessagesForSummarization(messages, 1000);
expect(result.toKeep).toHaveLength(4);
});
it('handles empty messages array', () => {
const result = selectMessagesForSummarization([], 1000);
expect(result.toSummarize).toHaveLength(0);
expect(result.toKeep).toHaveLength(0);
});
it('handles single message', () => {
const messages = [createMessageNode('user', 'Only message')];
const result = selectMessagesForSummarization(messages, 1000, 4);
expect(result.toSummarize).toHaveLength(0);
expect(result.toKeep).toHaveLength(1);
});
});
describe('calculateTokenSavings', () => {
it('calculates positive savings for longer original', () => {
const originalMessages = [
createMessageNode('user', 'This is a longer message with more words'),
createMessageNode('assistant', 'This is also a longer response with content')
];
const shortSummary = 'Brief summary.';
const savings = calculateTokenSavings(originalMessages, shortSummary);
expect(savings).toBeGreaterThan(0);
});
it('returns zero when summary is longer', () => {
const originalMessages = [createMessageNode('user', 'Hi')];
const longSummary =
'This is a very long summary that is much longer than the original message which was just a simple greeting.';
const savings = calculateTokenSavings(originalMessages, longSummary);
expect(savings).toBe(0);
});
});
describe('createSummaryRecord', () => {
it('creates a valid summary record', () => {
const record = createSummaryRecord('conv-123', 'This is the summary', 10, 500);
expect(record.id).toBeDefined();
expect(record.conversationId).toBe('conv-123');
expect(record.summary).toBe('This is the summary');
expect(record.originalMessageCount).toBe(10);
expect(record.tokensSaved).toBe(500);
expect(record.summarizedAt).toBeInstanceOf(Date);
});
it('generates unique IDs', () => {
const record1 = createSummaryRecord('conv-1', 'Summary 1', 5, 100);
const record2 = createSummaryRecord('conv-2', 'Summary 2', 5, 100);
expect(record1.id).not.toBe(record2.id);
});
});
describe('shouldSummarize', () => {
it('returns false when message count is too low', () => {
expect(shouldSummarize(8000, 10000, 4)).toBe(false);
expect(shouldSummarize(8000, 10000, 5)).toBe(false);
});
it('returns false when summarizable messages are too few', () => {
// 6 messages total - 4 preserved = 2 to summarize (minimum)
// But with < 6 total messages, should return false
expect(shouldSummarize(8000, 10000, 5)).toBe(false);
});
it('returns true when usage is high and enough messages', () => {
// 8000/10000 = 80%
expect(shouldSummarize(8000, 10000, 10)).toBe(true);
expect(shouldSummarize(9000, 10000, 8)).toBe(true);
});
it('returns false when usage is below 80%', () => {
expect(shouldSummarize(7000, 10000, 10)).toBe(false);
expect(shouldSummarize(5000, 10000, 20)).toBe(false);
});
it('returns true at exactly 80%', () => {
expect(shouldSummarize(8000, 10000, 10)).toBe(true);
});
});
describe('formatSummaryAsContext', () => {
it('formats summary as context prefix', () => {
const result = formatSummaryAsContext('User asked about weather');
expect(result).toBe('[Previous conversation summary: User asked about weather]');
});
it('handles empty summary', () => {
const result = formatSummaryAsContext('');
expect(result).toBe('[Previous conversation summary: ]');
});
it('preserves special characters in summary', () => {
const result = formatSummaryAsContext('User said "hello" & asked about <code>');
expect(result).toContain('"hello"');
expect(result).toContain('&');
expect(result).toContain('<code>');
});
});