agent-ecosystem/test/main/services/discovery/SearchTextCache.test.ts
matt 75dfcf2d50 feat: implement SearchTextCache and SearchTextExtractor for efficient text extraction and caching
- Added SearchTextCache for LRU caching of extracted search text with mtime invalidation.
- Introduced SearchTextExtractor for lightweight extraction of searchable text from session messages.
- Updated SessionSearcher to utilize the new extractor and cache for improved search performance.
- Added tests for SearchTextCache and SearchTextExtractor to ensure functionality and correctness.
2026-02-22 02:03:22 +09:00

119 lines
4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { SearchTextCache } from '../../../../src/main/services/discovery/SearchTextCache';
import type { SearchableEntry } from '../../../../src/main/services/discovery/SearchTextExtractor';
function makeEntry(text: string, groupId: string): SearchableEntry {
return {
text,
groupId,
messageType: 'user',
itemType: 'user',
timestamp: Date.now(),
messageUuid: groupId,
};
}
describe('SearchTextCache', () => {
it('returns cached entry on mtime match', () => {
const cache = new SearchTextCache();
const entries = [makeEntry('hello', 'user-1')];
cache.set('/path/a.jsonl', 1000, entries, 'Title A');
const result = cache.get('/path/a.jsonl', 1000);
expect(result).toBeDefined();
expect(result!.entries).toEqual(entries);
expect(result!.sessionTitle).toBe('Title A');
});
it('returns undefined on mtime mismatch (stale)', () => {
const cache = new SearchTextCache();
const entries = [makeEntry('hello', 'user-1')];
cache.set('/path/a.jsonl', 1000, entries, 'Title A');
const result = cache.get('/path/a.jsonl', 2000);
expect(result).toBeUndefined();
});
it('returns undefined for uncached paths', () => {
const cache = new SearchTextCache();
const result = cache.get('/path/missing.jsonl', 1000);
expect(result).toBeUndefined();
});
it('evicts oldest entry when at max capacity', () => {
const cache = new SearchTextCache(3);
cache.set('/path/1.jsonl', 100, [makeEntry('one', 'u1')], 'One');
cache.set('/path/2.jsonl', 200, [makeEntry('two', 'u2')], 'Two');
cache.set('/path/3.jsonl', 300, [makeEntry('three', 'u3')], 'Three');
expect(cache.size).toBe(3);
// Adding a 4th entry should evict the oldest (1.jsonl)
cache.set('/path/4.jsonl', 400, [makeEntry('four', 'u4')], 'Four');
expect(cache.size).toBe(3);
expect(cache.get('/path/1.jsonl', 100)).toBeUndefined();
expect(cache.get('/path/4.jsonl', 400)).toBeDefined();
});
it('LRU access moves entry to end, preserving it from eviction', () => {
const cache = new SearchTextCache(3);
cache.set('/path/1.jsonl', 100, [makeEntry('one', 'u1')], 'One');
cache.set('/path/2.jsonl', 200, [makeEntry('two', 'u2')], 'Two');
cache.set('/path/3.jsonl', 300, [makeEntry('three', 'u3')], 'Three');
// Access entry 1, moving it to end
cache.get('/path/1.jsonl', 100);
// Adding a 4th should now evict entry 2 (oldest after LRU access)
cache.set('/path/4.jsonl', 400, [makeEntry('four', 'u4')], 'Four');
expect(cache.get('/path/1.jsonl', 100)).toBeDefined();
expect(cache.get('/path/2.jsonl', 200)).toBeUndefined();
});
it('invalidate() removes a specific entry', () => {
const cache = new SearchTextCache();
cache.set('/path/a.jsonl', 1000, [makeEntry('hello', 'u1')], 'Title');
cache.invalidate('/path/a.jsonl');
expect(cache.get('/path/a.jsonl', 1000)).toBeUndefined();
expect(cache.size).toBe(0);
});
it('clear() empties the cache', () => {
const cache = new SearchTextCache();
cache.set('/path/1.jsonl', 100, [makeEntry('one', 'u1')], 'One');
cache.set('/path/2.jsonl', 200, [makeEntry('two', 'u2')], 'Two');
expect(cache.size).toBe(2);
cache.clear();
expect(cache.size).toBe(0);
});
it('handles undefined sessionTitle', () => {
const cache = new SearchTextCache();
cache.set('/path/a.jsonl', 1000, [], undefined);
const result = cache.get('/path/a.jsonl', 1000);
expect(result).toBeDefined();
expect(result!.sessionTitle).toBeUndefined();
expect(result!.entries).toEqual([]);
});
it('updates existing entry on re-set', () => {
const cache = new SearchTextCache();
cache.set('/path/a.jsonl', 1000, [makeEntry('old', 'u1')], 'Old');
cache.set('/path/a.jsonl', 2000, [makeEntry('new', 'u2')], 'New');
const result = cache.get('/path/a.jsonl', 2000);
expect(result).toBeDefined();
expect(result!.entries[0].text).toBe('new');
expect(result!.sessionTitle).toBe('New');
expect(cache.size).toBe(1);
});
});