test: add tests for mergeWithCache, exportAsMarkdown, and slashUtils (127 total)
This commit is contained in:
87
src/lib/__tests__/exportConversation.test.ts
Normal file
87
src/lib/__tests__/exportConversation.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { exportAsMarkdown } from '../exportConversation';
|
||||
import type { ChatMessage } from '../../types';
|
||||
|
||||
function makeMsg(
|
||||
role: 'user' | 'assistant',
|
||||
content: string,
|
||||
blocks: ChatMessage['blocks'] = [],
|
||||
): ChatMessage {
|
||||
return { id: `msg-${Math.random()}`, role, content, timestamp: 1700000000000, blocks };
|
||||
}
|
||||
|
||||
describe('exportAsMarkdown', () => {
|
||||
it('exports a basic user + assistant exchange', () => {
|
||||
const msgs = [
|
||||
makeMsg('user', 'Hello'),
|
||||
makeMsg('assistant', 'Hi there!'),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('# Conversation');
|
||||
expect(md).toContain('### 👤 User');
|
||||
expect(md).toContain('Hello');
|
||||
expect(md).toContain('### 🤖 Assistant');
|
||||
expect(md).toContain('Hi there!');
|
||||
});
|
||||
|
||||
it('uses custom session label as title', () => {
|
||||
const md = exportAsMarkdown([], 'My Session');
|
||||
expect(md).toContain('# My Session');
|
||||
});
|
||||
|
||||
it('renders text blocks', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'text', text: 'Block content' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('Block content');
|
||||
});
|
||||
|
||||
it('renders thinking blocks in details tags', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'thinking', text: 'Deep thought' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('<details>');
|
||||
expect(md).toContain('💭 Thinking');
|
||||
expect(md).toContain('Deep thought');
|
||||
expect(md).toContain('</details>');
|
||||
});
|
||||
|
||||
it('renders tool_use blocks with JSON', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'tool_use', name: 'exec', input: { command: 'ls' }, id: 't1' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('**🔧 Tool: `exec`**');
|
||||
expect(md).toContain('"command": "ls"');
|
||||
});
|
||||
|
||||
it('renders tool_result blocks truncated', () => {
|
||||
const longContent = 'x'.repeat(3000);
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'tool_result', content: longContent, tool_use_id: 't1' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('**📋 Result:**');
|
||||
expect(md).toContain('...(truncated)');
|
||||
// Should have at most 2000 chars of content + truncation
|
||||
const resultLine = md.split('\n').find(l => l.startsWith('xxx'));
|
||||
expect(resultLine!.length).toBeLessThanOrEqual(2000);
|
||||
});
|
||||
|
||||
it('renders compaction separator', () => {
|
||||
const msgs: ChatMessage[] = [
|
||||
{ id: 'sep', role: 'assistant', content: '', timestamp: 0, blocks: [], isCompactionSeparator: true },
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('---');
|
||||
expect(md).toContain('*Context compacted*');
|
||||
});
|
||||
|
||||
it('falls back to content when no blocks', () => {
|
||||
const msgs = [makeMsg('user', 'Plain text')];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('Plain text');
|
||||
});
|
||||
});
|
||||
60
src/lib/__tests__/messageCache.test.ts
Normal file
60
src/lib/__tests__/messageCache.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mergeWithCache } from '../messageCache';
|
||||
import type { ChatMessage } from '../../types';
|
||||
|
||||
function makeMsg(id: string, role: 'user' | 'assistant' = 'user', ts = Date.now()): ChatMessage {
|
||||
return { id, role, content: `msg-${id}`, timestamp: ts, blocks: [] };
|
||||
}
|
||||
|
||||
describe('mergeWithCache', () => {
|
||||
it('returns gateway messages unchanged when cache is empty', () => {
|
||||
const gateway = [makeMsg('a'), makeMsg('b')];
|
||||
const result = mergeWithCache(gateway, []);
|
||||
expect(result.messages).toEqual(gateway);
|
||||
expect(result.wasCompacted).toBe(false);
|
||||
});
|
||||
|
||||
it('returns gateway messages when cache has same messages', () => {
|
||||
const msgs = [makeMsg('a'), makeMsg('b')];
|
||||
const result = mergeWithCache(msgs, msgs);
|
||||
expect(result.messages).toEqual(msgs);
|
||||
expect(result.wasCompacted).toBe(false);
|
||||
});
|
||||
|
||||
it('detects compaction and merges old cached messages', () => {
|
||||
const old1 = makeMsg('old1', 'user', 1000);
|
||||
const old2 = makeMsg('old2', 'assistant', 2000);
|
||||
const current = makeMsg('new1', 'user', 5000);
|
||||
|
||||
const cached = [old1, old2, current];
|
||||
const gateway = [current]; // old messages compacted away
|
||||
|
||||
const result = mergeWithCache(gateway, cached);
|
||||
expect(result.wasCompacted).toBe(true);
|
||||
// Should have: old1 (archived), old2 (archived), separator, new1
|
||||
expect(result.messages).toHaveLength(4);
|
||||
expect(result.messages[0]).toMatchObject({ id: 'old1', isArchived: true });
|
||||
expect(result.messages[1]).toMatchObject({ id: 'old2', isArchived: true });
|
||||
expect(result.messages[2].isCompactionSeparator).toBe(true);
|
||||
expect(result.messages[3].id).toBe('new1');
|
||||
});
|
||||
|
||||
it('separator timestamp is just before first gateway message', () => {
|
||||
const cached = [makeMsg('old', 'user', 1000)];
|
||||
const gateway = [makeMsg('new', 'user', 5000)];
|
||||
|
||||
const result = mergeWithCache(gateway, cached);
|
||||
const separator = result.messages.find(m => m.isCompactionSeparator);
|
||||
expect(separator).toBeDefined();
|
||||
expect(separator!.timestamp).toBe(4999);
|
||||
});
|
||||
|
||||
it('handles empty gateway messages with non-empty cache', () => {
|
||||
const cached = [makeMsg('old', 'user', 1000)];
|
||||
const result = mergeWithCache([], cached);
|
||||
expect(result.wasCompacted).toBe(true);
|
||||
expect(result.messages).toHaveLength(2); // archived + separator
|
||||
expect(result.messages[0]).toMatchObject({ id: 'old', isArchived: true });
|
||||
expect(result.messages[1].isCompactionSeparator).toBe(true);
|
||||
});
|
||||
});
|
||||
28
src/lib/__tests__/slashUtils.test.ts
Normal file
28
src/lib/__tests__/slashUtils.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { shouldShowSlashMenu } from '../slashUtils';
|
||||
|
||||
describe('shouldShowSlashMenu', () => {
|
||||
it('returns true for a slash at the start', () => {
|
||||
expect(shouldShowSlashMenu('/status')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for slash with leading spaces', () => {
|
||||
expect(shouldShowSlashMenu(' /help')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for text without slash', () => {
|
||||
expect(shouldShowSlashMenu('hello')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if slash is not at start', () => {
|
||||
expect(shouldShowSlashMenu('hello /cmd')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if text contains newline', () => {
|
||||
expect(shouldShowSlashMenu('/cmd\nmore')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for just a slash', () => {
|
||||
expect(shouldShowSlashMenu('/')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user