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
215 lines
6.5 KiB
TypeScript
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>');
|
|
});
|
|
});
|