agent-ecosystem/test/main/services/discovery/SessionSearcher.test.ts
iliya c21350713c perf: replace remark-based search with plain text indexOf
Manually ported from upstream 5c7f921e. Key changes:
- SessionSearcher: indexOf instead of remark AST, batch size 8→16
- conversationSlice: indexOf with MAX_SEARCH_MATCHES=500 cap
- Item-scoped store selectors (searchMatchItemIds Set) to skip re-renders
- Pre-filter in markdownTextSearch (skip parse if no raw match)
- SearchTextCache: 200→1000 entries
- ProjectScanner: 30s search project cache, batch 4→8
2026-03-25 14:32:37 +02:00

120 lines
4.2 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { SessionSearcher } from '../../../../src/main/services/discovery/SessionSearcher';
describe('SessionSearcher', () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
}
tempDirs.length = 0;
});
it('searches only user text and AI last text output, returning every match occurrence', async () => {
const projectsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-searcher-'));
tempDirs.push(projectsDir);
const projectId = 'project-1';
const sessionId = 'session-1';
const projectPath = path.join(projectsDir, projectId);
fs.mkdirSync(projectPath, { recursive: true });
const sessionPath = path.join(projectPath, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({
uuid: 'user-1',
type: 'user',
timestamp: '2026-01-01T00:00:00.000Z',
message: { role: 'user', content: 'alpha intro alpha' },
isMeta: false,
}),
JSON.stringify({
uuid: 'asst-1',
type: 'assistant',
timestamp: '2026-01-01T00:00:01.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'older alpha that should be ignored' }],
},
}),
JSON.stringify({
uuid: 'asst-2',
type: 'assistant',
timestamp: '2026-01-01T00:00:02.000Z',
message: {
role: 'assistant',
content: [
{ type: 'thinking', thinking: 'alpha in thinking should not be matched' },
{ type: 'text', text: 'latest alpha alpha output' },
],
},
}),
];
fs.writeFileSync(sessionPath, `${lines.join('\n')}\n`, 'utf8');
const searcher = new SessionSearcher(projectsDir);
const result = await searcher.searchSessions(projectId, 'alpha', 50);
expect(result.totalMatches).toBe(4);
expect(result.results).toHaveLength(4);
const userResults = result.results.filter((entry) => entry.groupId === 'user-user-1');
const aiResults = result.results.filter((entry) => entry.groupId === 'ai-asst-1');
expect(userResults).toHaveLength(2);
expect(aiResults).toHaveLength(2);
expect(userResults.map((entry) => entry.matchIndexInItem)).toEqual([0, 1]);
expect(aiResults.map((entry) => entry.matchIndexInItem)).toEqual([0, 1]);
expect(result.results.some((entry) => entry.context.includes('ignored'))).toBe(false);
expect(
result.results.every((entry) => entry.itemType === 'user' || entry.itemType === 'ai')
).toBe(true);
});
it('matches text in code fences with plain text search', async () => {
const projectsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-searcher-md-'));
tempDirs.push(projectsDir);
const projectId = 'project-2';
const sessionId = 'session-2';
const projectPath = path.join(projectsDir, projectId);
fs.mkdirSync(projectPath, { recursive: true });
const sessionPath = path.join(projectPath, `${sessionId}.jsonl`);
const codeBlock = '```tsx\nconst x = 1;\n```';
const lines = [
JSON.stringify({
uuid: 'user-md-1',
type: 'user',
timestamp: '2026-01-01T00:00:00.000Z',
message: { role: 'user', content: 'Show me tsx code' },
isMeta: false,
}),
JSON.stringify({
uuid: 'asst-md-1',
type: 'assistant',
timestamp: '2026-01-01T00:00:01.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: `Here is a code block:\n\n${codeBlock}` }],
},
}),
];
fs.writeFileSync(sessionPath, `${lines.join('\n')}\n`, 'utf8');
const searcher = new SessionSearcher(projectsDir);
const result = await searcher.searchSessions(projectId, 'tsx', 50);
// Plain text search: "tsx" matches in user text AND in the code fence identifier
const userResults = result.results.filter((r) => r.itemType === 'user');
const aiResults = result.results.filter((r) => r.itemType === 'ai');
expect(userResults).toHaveLength(1);
expect(aiResults).toHaveLength(1);
});
});