viberag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +219 -0
- package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
- package/dist/cli/__tests__/mcp-setup.test.js +597 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +238 -0
- package/dist/cli/commands/handlers.d.ts +57 -0
- package/dist/cli/commands/handlers.js +231 -0
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/mcp-setup.d.ts +107 -0
- package/dist/cli/commands/mcp-setup.js +509 -0
- package/dist/cli/commands/useRagCommands.d.ts +23 -0
- package/dist/cli/commands/useRagCommands.js +180 -0
- package/dist/cli/components/CleanWizard.d.ts +17 -0
- package/dist/cli/components/CleanWizard.js +169 -0
- package/dist/cli/components/InitWizard.d.ts +20 -0
- package/dist/cli/components/InitWizard.js +370 -0
- package/dist/cli/components/McpSetupWizard.d.ts +37 -0
- package/dist/cli/components/McpSetupWizard.js +387 -0
- package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
- package/dist/cli/components/SearchResultsDisplay.js +130 -0
- package/dist/cli/components/WelcomeBanner.d.ts +10 -0
- package/dist/cli/components/WelcomeBanner.js +26 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/data/mcp-editors.d.ts +80 -0
- package/dist/cli/data/mcp-editors.js +270 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli-bundle.cjs +5269 -0
- package/dist/common/commands/terminalSetup.d.ts +2 -0
- package/dist/common/commands/terminalSetup.js +144 -0
- package/dist/common/components/CommandSuggestions.d.ts +9 -0
- package/dist/common/components/CommandSuggestions.js +20 -0
- package/dist/common/components/StaticWithResize.d.ts +23 -0
- package/dist/common/components/StaticWithResize.js +62 -0
- package/dist/common/components/StatusBar.d.ts +8 -0
- package/dist/common/components/StatusBar.js +64 -0
- package/dist/common/components/TextInput.d.ts +12 -0
- package/dist/common/components/TextInput.js +239 -0
- package/dist/common/components/index.d.ts +3 -0
- package/dist/common/components/index.js +3 -0
- package/dist/common/hooks/index.d.ts +4 -0
- package/dist/common/hooks/index.js +4 -0
- package/dist/common/hooks/useCommandHistory.d.ts +7 -0
- package/dist/common/hooks/useCommandHistory.js +51 -0
- package/dist/common/hooks/useCtrlC.d.ts +9 -0
- package/dist/common/hooks/useCtrlC.js +40 -0
- package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
- package/dist/common/hooks/useKittyKeyboard.js +26 -0
- package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
- package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
- package/dist/common/hooks/useTerminalResize.d.ts +28 -0
- package/dist/common/hooks/useTerminalResize.js +51 -0
- package/dist/common/hooks/useTextBuffer.d.ts +13 -0
- package/dist/common/hooks/useTextBuffer.js +165 -0
- package/dist/common/index.d.ts +13 -0
- package/dist/common/index.js +17 -0
- package/dist/common/types.d.ts +162 -0
- package/dist/common/types.js +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +66 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +837 -0
- package/dist/mcp/watcher.d.ts +86 -0
- package/dist/mcp/watcher.js +334 -0
- package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
- package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
- package/dist/rag/__tests__/helpers.d.ts +30 -0
- package/dist/rag/__tests__/helpers.js +67 -0
- package/dist/rag/__tests__/merkle.test.d.ts +5 -0
- package/dist/rag/__tests__/merkle.test.js +161 -0
- package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
- package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
- package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
- package/dist/rag/__tests__/multi-language.test.js +535 -0
- package/dist/rag/__tests__/rag.test.d.ts +10 -0
- package/dist/rag/__tests__/rag.test.js +311 -0
- package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
- package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
- package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
- package/dist/rag/__tests__/search-filters.test.js +250 -0
- package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
- package/dist/rag/__tests__/search-modes.test.js +133 -0
- package/dist/rag/config/index.d.ts +61 -0
- package/dist/rag/config/index.js +111 -0
- package/dist/rag/constants.d.ts +41 -0
- package/dist/rag/constants.js +57 -0
- package/dist/rag/embeddings/fastembed.d.ts +62 -0
- package/dist/rag/embeddings/fastembed.js +124 -0
- package/dist/rag/embeddings/gemini.d.ts +26 -0
- package/dist/rag/embeddings/gemini.js +116 -0
- package/dist/rag/embeddings/index.d.ts +10 -0
- package/dist/rag/embeddings/index.js +9 -0
- package/dist/rag/embeddings/local-4b.d.ts +28 -0
- package/dist/rag/embeddings/local-4b.js +51 -0
- package/dist/rag/embeddings/local.d.ts +29 -0
- package/dist/rag/embeddings/local.js +119 -0
- package/dist/rag/embeddings/mistral.d.ts +22 -0
- package/dist/rag/embeddings/mistral.js +85 -0
- package/dist/rag/embeddings/openai.d.ts +22 -0
- package/dist/rag/embeddings/openai.js +85 -0
- package/dist/rag/embeddings/types.d.ts +37 -0
- package/dist/rag/embeddings/types.js +1 -0
- package/dist/rag/gitignore/index.d.ts +57 -0
- package/dist/rag/gitignore/index.js +178 -0
- package/dist/rag/index.d.ts +15 -0
- package/dist/rag/index.js +25 -0
- package/dist/rag/indexer/chunker.d.ts +129 -0
- package/dist/rag/indexer/chunker.js +1352 -0
- package/dist/rag/indexer/index.d.ts +6 -0
- package/dist/rag/indexer/index.js +6 -0
- package/dist/rag/indexer/indexer.d.ts +73 -0
- package/dist/rag/indexer/indexer.js +356 -0
- package/dist/rag/indexer/types.d.ts +68 -0
- package/dist/rag/indexer/types.js +47 -0
- package/dist/rag/logger/index.d.ts +20 -0
- package/dist/rag/logger/index.js +75 -0
- package/dist/rag/manifest/index.d.ts +50 -0
- package/dist/rag/manifest/index.js +97 -0
- package/dist/rag/merkle/diff.d.ts +26 -0
- package/dist/rag/merkle/diff.js +95 -0
- package/dist/rag/merkle/hash.d.ts +34 -0
- package/dist/rag/merkle/hash.js +165 -0
- package/dist/rag/merkle/index.d.ts +68 -0
- package/dist/rag/merkle/index.js +298 -0
- package/dist/rag/merkle/node.d.ts +51 -0
- package/dist/rag/merkle/node.js +69 -0
- package/dist/rag/search/filters.d.ts +21 -0
- package/dist/rag/search/filters.js +100 -0
- package/dist/rag/search/fts.d.ts +32 -0
- package/dist/rag/search/fts.js +61 -0
- package/dist/rag/search/hybrid.d.ts +17 -0
- package/dist/rag/search/hybrid.js +58 -0
- package/dist/rag/search/index.d.ts +89 -0
- package/dist/rag/search/index.js +367 -0
- package/dist/rag/search/types.d.ts +130 -0
- package/dist/rag/search/types.js +4 -0
- package/dist/rag/search/vector.d.ts +25 -0
- package/dist/rag/search/vector.js +44 -0
- package/dist/rag/storage/index.d.ts +92 -0
- package/dist/rag/storage/index.js +287 -0
- package/dist/rag/storage/lancedb-native.d.ts +7 -0
- package/dist/rag/storage/lancedb-native.js +10 -0
- package/dist/rag/storage/schema.d.ts +23 -0
- package/dist/rag/storage/schema.js +50 -0
- package/dist/rag/storage/types.d.ts +100 -0
- package/dist/rag/storage/types.js +68 -0
- package/package.json +67 -0
- package/scripts/check-node-version.js +37 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for the RAG system.
|
|
3
|
+
*
|
|
4
|
+
* Tests system behavior, not library correctness:
|
|
5
|
+
* - Merkle tree correctly detects file changes
|
|
6
|
+
* - Search returns expected files for known queries
|
|
7
|
+
* - Incremental indexing only reprocesses what changed
|
|
8
|
+
* - Manifest persistence enables recovery
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { Indexer } from '../indexer/indexer.js';
|
|
12
|
+
import { SearchEngine } from '../search/index.js';
|
|
13
|
+
import { copyFixtureToTemp, addFile, modifyFile, deleteFile, waitForFs, } from './helpers.js';
|
|
14
|
+
describe('RAG E2E', () => {
|
|
15
|
+
let ctx;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await ctx.cleanup();
|
|
21
|
+
});
|
|
22
|
+
it('indexes codebase and finds files by semantic search', async () => {
|
|
23
|
+
// Index fixture
|
|
24
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
25
|
+
await indexer.index();
|
|
26
|
+
indexer.close();
|
|
27
|
+
// Search for math operations → should find math.py
|
|
28
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
29
|
+
const mathResults = await search.search('add two numbers calculate sum');
|
|
30
|
+
expect(mathResults.results.some(r => r.filepath.includes('math.py'))).toBe(true);
|
|
31
|
+
// Search for HTTP/API → should find http_client.ts
|
|
32
|
+
const httpResults = await search.search('fetch data API request');
|
|
33
|
+
expect(httpResults.results.some(r => r.filepath.includes('http_client.ts'))).toBe(true);
|
|
34
|
+
// Search for string/date → should find utils.js
|
|
35
|
+
const utilsResults = await search.search('format string parse date');
|
|
36
|
+
expect(utilsResults.results.some(r => r.filepath.includes('utils.js'))).toBe(true);
|
|
37
|
+
search.close();
|
|
38
|
+
}, 60000);
|
|
39
|
+
it('detects new/modified/deleted files correctly', async () => {
|
|
40
|
+
// Initial index
|
|
41
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
42
|
+
await indexer.index();
|
|
43
|
+
indexer.close();
|
|
44
|
+
// Add new file
|
|
45
|
+
await addFile(ctx.projectRoot, 'new_module.py', 'def new_function():\n return "new"\n');
|
|
46
|
+
// Modify existing file
|
|
47
|
+
await modifyFile(ctx.projectRoot, 'math.py', '# Modified file\ndef add(a, b):\n return a + b\n');
|
|
48
|
+
// Delete a file
|
|
49
|
+
await deleteFile(ctx.projectRoot, 'utils.js');
|
|
50
|
+
await waitForFs();
|
|
51
|
+
// Reindex
|
|
52
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
53
|
+
const stats = await indexer2.index();
|
|
54
|
+
indexer2.close();
|
|
55
|
+
expect(stats.filesNew).toBe(1);
|
|
56
|
+
expect(stats.filesModified).toBe(1);
|
|
57
|
+
expect(stats.filesDeleted).toBe(1);
|
|
58
|
+
}, 60000);
|
|
59
|
+
it('skips unchanged files on reindex', async () => {
|
|
60
|
+
// Initial index
|
|
61
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
62
|
+
await indexer.index();
|
|
63
|
+
indexer.close();
|
|
64
|
+
// Reindex with no changes
|
|
65
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
66
|
+
const stats = await indexer2.index();
|
|
67
|
+
indexer2.close();
|
|
68
|
+
expect(stats.filesNew).toBe(0);
|
|
69
|
+
expect(stats.filesModified).toBe(0);
|
|
70
|
+
expect(stats.filesDeleted).toBe(0);
|
|
71
|
+
expect(stats.chunksAdded).toBe(0);
|
|
72
|
+
}, 60000);
|
|
73
|
+
it('recovers state from manifest after restart', async () => {
|
|
74
|
+
// Index
|
|
75
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
76
|
+
await indexer.index();
|
|
77
|
+
indexer.close();
|
|
78
|
+
// "Restart" - create new indexer instance
|
|
79
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
80
|
+
const stats = await indexer2.index();
|
|
81
|
+
indexer2.close();
|
|
82
|
+
// Should detect no changes (recovered from manifest)
|
|
83
|
+
expect(stats.filesNew).toBe(0);
|
|
84
|
+
expect(stats.filesModified).toBe(0);
|
|
85
|
+
expect(stats.filesDeleted).toBe(0);
|
|
86
|
+
}, 60000);
|
|
87
|
+
it('reindexes all files with force=true', async () => {
|
|
88
|
+
// Initial index
|
|
89
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
90
|
+
await indexer.index();
|
|
91
|
+
indexer.close();
|
|
92
|
+
// Force reindex
|
|
93
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
94
|
+
const stats = await indexer2.index({ force: true });
|
|
95
|
+
indexer2.close();
|
|
96
|
+
// All files treated as new (5 files in fixture)
|
|
97
|
+
expect(stats.filesNew).toBeGreaterThanOrEqual(4); // fixture file count (empty.py may not produce chunks)
|
|
98
|
+
expect(stats.chunksAdded).toBeGreaterThan(0);
|
|
99
|
+
// Embeddings should be cached from first run
|
|
100
|
+
expect(stats.embeddingsCached).toBeGreaterThan(0);
|
|
101
|
+
}, 60000);
|
|
102
|
+
it('removes deleted files from search results', async () => {
|
|
103
|
+
// Initial index
|
|
104
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
105
|
+
await indexer.index();
|
|
106
|
+
indexer.close();
|
|
107
|
+
// Verify math.py is searchable
|
|
108
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
109
|
+
let results = await search.search('add two numbers');
|
|
110
|
+
expect(results.results.some(r => r.filepath.includes('math.py'))).toBe(true);
|
|
111
|
+
search.close();
|
|
112
|
+
// Delete math.py
|
|
113
|
+
await deleteFile(ctx.projectRoot, 'math.py');
|
|
114
|
+
await waitForFs();
|
|
115
|
+
// Reindex
|
|
116
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
117
|
+
await indexer2.index();
|
|
118
|
+
indexer2.close();
|
|
119
|
+
// Should no longer appear in results
|
|
120
|
+
const search2 = new SearchEngine(ctx.projectRoot);
|
|
121
|
+
results = await search2.search('add two numbers');
|
|
122
|
+
expect(results.results.some(r => r.filepath.includes('math.py'))).toBe(false);
|
|
123
|
+
search2.close();
|
|
124
|
+
}, 60000);
|
|
125
|
+
});
|
|
126
|
+
describe('Search modes', () => {
|
|
127
|
+
let ctx;
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
130
|
+
// Index the fixture
|
|
131
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
132
|
+
await indexer.index();
|
|
133
|
+
indexer.close();
|
|
134
|
+
});
|
|
135
|
+
afterEach(async () => {
|
|
136
|
+
await ctx.cleanup();
|
|
137
|
+
});
|
|
138
|
+
it('vector search finds semantically similar content', async () => {
|
|
139
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
140
|
+
// "calculate sum" should find math.py via semantic similarity
|
|
141
|
+
const results = await search.searchVector('calculate sum', 5);
|
|
142
|
+
expect(results.results.some(r => r.filepath.includes('math.py'))).toBe(true);
|
|
143
|
+
search.close();
|
|
144
|
+
}, 60000);
|
|
145
|
+
it('FTS search finds exact keyword matches', async () => {
|
|
146
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
147
|
+
// "fetchData" exact match in http_client.ts
|
|
148
|
+
const results = await search.searchFts('fetchData', 5);
|
|
149
|
+
expect(results.results.some(r => r.filepath.includes('http_client.ts'))).toBe(true);
|
|
150
|
+
search.close();
|
|
151
|
+
}, 60000);
|
|
152
|
+
it('hybrid search returns results with both scores', async () => {
|
|
153
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
154
|
+
const results = await search.search('API request fetch');
|
|
155
|
+
expect(results.results.some(r => r.filepath.includes('http_client.ts'))).toBe(true);
|
|
156
|
+
// Results should have both vector and FTS scores
|
|
157
|
+
if (results.results.length > 0) {
|
|
158
|
+
expect(results.results[0]).toHaveProperty('vectorScore');
|
|
159
|
+
expect(results.results[0]).toHaveProperty('ftsScore');
|
|
160
|
+
}
|
|
161
|
+
search.close();
|
|
162
|
+
}, 60000);
|
|
163
|
+
});
|
|
164
|
+
describe('Edge cases', () => {
|
|
165
|
+
let ctx;
|
|
166
|
+
beforeEach(async () => {
|
|
167
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
168
|
+
});
|
|
169
|
+
afterEach(async () => {
|
|
170
|
+
await ctx.cleanup();
|
|
171
|
+
});
|
|
172
|
+
it('handles empty files gracefully', async () => {
|
|
173
|
+
// empty.py is already in fixture (0 bytes)
|
|
174
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
175
|
+
const stats = await indexer.index();
|
|
176
|
+
indexer.close();
|
|
177
|
+
// Should not crash, empty file counted but produces no chunks
|
|
178
|
+
expect(stats.filesScanned).toBeGreaterThan(0);
|
|
179
|
+
}, 60000);
|
|
180
|
+
it('indexes files with unicode content', async () => {
|
|
181
|
+
// unicode_content.js has Korean, emoji, Chinese
|
|
182
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
183
|
+
await indexer.index();
|
|
184
|
+
indexer.close();
|
|
185
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
186
|
+
const results = await search.search('Korean greeting emoji');
|
|
187
|
+
expect(results.results.some(r => r.filepath.includes('unicode_content'))).toBe(true);
|
|
188
|
+
search.close();
|
|
189
|
+
}, 60000);
|
|
190
|
+
it('handles files with syntax errors gracefully', async () => {
|
|
191
|
+
// Add malformed file
|
|
192
|
+
await addFile(ctx.projectRoot, 'broken.ts', 'function { broken syntax');
|
|
193
|
+
await waitForFs();
|
|
194
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
195
|
+
const stats = await indexer.index();
|
|
196
|
+
indexer.close();
|
|
197
|
+
// Should still index (as module chunk), not crash
|
|
198
|
+
expect(stats.chunksAdded).toBeGreaterThan(0);
|
|
199
|
+
}, 60000);
|
|
200
|
+
});
|
|
201
|
+
describe('Error handling', () => {
|
|
202
|
+
let ctx;
|
|
203
|
+
beforeEach(async () => {
|
|
204
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
205
|
+
});
|
|
206
|
+
afterEach(async () => {
|
|
207
|
+
await ctx.cleanup();
|
|
208
|
+
});
|
|
209
|
+
it('continues indexing when one file fails to parse', async () => {
|
|
210
|
+
// Add broken file alongside good files
|
|
211
|
+
await addFile(ctx.projectRoot, 'broken.ts', 'const x = {{{');
|
|
212
|
+
await addFile(ctx.projectRoot, 'good.ts', 'export function works() { return 1; }');
|
|
213
|
+
await waitForFs();
|
|
214
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
215
|
+
const stats = await indexer.index();
|
|
216
|
+
indexer.close();
|
|
217
|
+
// Both files processed
|
|
218
|
+
expect(stats.chunksAdded).toBeGreaterThan(0);
|
|
219
|
+
// Good file should be searchable
|
|
220
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
221
|
+
const results = await search.search('works function export');
|
|
222
|
+
expect(results.results.some(r => r.filepath.includes('good.ts'))).toBe(true);
|
|
223
|
+
search.close();
|
|
224
|
+
}, 60000);
|
|
225
|
+
it('skips binary files without error', async () => {
|
|
226
|
+
// Add binary content (PNG header bytes)
|
|
227
|
+
const binaryContent = Buffer.from([
|
|
228
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
229
|
+
]).toString('binary');
|
|
230
|
+
await addFile(ctx.projectRoot, 'image.png', binaryContent);
|
|
231
|
+
await waitForFs();
|
|
232
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
233
|
+
// Should not crash
|
|
234
|
+
const stats = await indexer.index();
|
|
235
|
+
indexer.close();
|
|
236
|
+
// Index should complete successfully
|
|
237
|
+
expect(stats.filesScanned).toBeGreaterThan(0);
|
|
238
|
+
}, 60000);
|
|
239
|
+
});
|
|
240
|
+
describe('Subdirectory indexing', () => {
|
|
241
|
+
let ctx;
|
|
242
|
+
beforeEach(async () => {
|
|
243
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
244
|
+
});
|
|
245
|
+
afterEach(async () => {
|
|
246
|
+
await ctx.cleanup();
|
|
247
|
+
});
|
|
248
|
+
it('indexes files in nested subdirectories', async () => {
|
|
249
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
250
|
+
const stats = await indexer.index();
|
|
251
|
+
indexer.close();
|
|
252
|
+
// Should have indexed all nested files (5 original + 10 nested = 15)
|
|
253
|
+
// Note: empty.py may not produce chunks
|
|
254
|
+
expect(stats.filesScanned).toBeGreaterThanOrEqual(10);
|
|
255
|
+
// Should find deeply nested file via search
|
|
256
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
257
|
+
const results = await search.search('flatten deeply nested array');
|
|
258
|
+
expect(results.results.some(r => r.filepath.includes('deep/nested'))).toBe(true);
|
|
259
|
+
search.close();
|
|
260
|
+
}, 60000);
|
|
261
|
+
it('detects changes in deeply nested files', async () => {
|
|
262
|
+
// Initial index
|
|
263
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
264
|
+
await indexer.index();
|
|
265
|
+
indexer.close();
|
|
266
|
+
// Modify file 3 levels deep
|
|
267
|
+
await modifyFile(ctx.projectRoot, 'src/components/forms/LoginForm.tsx', '// modified login form\nexport function LoginForm() { return null; }');
|
|
268
|
+
await waitForFs();
|
|
269
|
+
// Reindex
|
|
270
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
271
|
+
const stats = await indexer2.index();
|
|
272
|
+
indexer2.close();
|
|
273
|
+
expect(stats.filesModified).toBe(1);
|
|
274
|
+
}, 60000);
|
|
275
|
+
it('removes chunks when nested file deleted', async () => {
|
|
276
|
+
// Initial index
|
|
277
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
278
|
+
await indexer.index();
|
|
279
|
+
indexer.close();
|
|
280
|
+
// Verify file is searchable
|
|
281
|
+
const search1 = new SearchEngine(ctx.projectRoot);
|
|
282
|
+
let results = await search1.search('LoginForm authentication login');
|
|
283
|
+
expect(results.results.some(r => r.filepath.includes('LoginForm'))).toBe(true);
|
|
284
|
+
search1.close();
|
|
285
|
+
// Delete nested file
|
|
286
|
+
await deleteFile(ctx.projectRoot, 'src/components/forms/LoginForm.tsx');
|
|
287
|
+
await waitForFs();
|
|
288
|
+
// Reindex
|
|
289
|
+
const indexer2 = new Indexer(ctx.projectRoot);
|
|
290
|
+
const stats = await indexer2.index();
|
|
291
|
+
indexer2.close();
|
|
292
|
+
expect(stats.filesDeleted).toBe(1);
|
|
293
|
+
// Should no longer be searchable
|
|
294
|
+
const search2 = new SearchEngine(ctx.projectRoot);
|
|
295
|
+
results = await search2.search('LoginForm authentication login');
|
|
296
|
+
expect(results.results.some(r => r.filepath.includes('LoginForm'))).toBe(false);
|
|
297
|
+
search2.close();
|
|
298
|
+
}, 60000);
|
|
299
|
+
it('indexes sibling files in same directory', async () => {
|
|
300
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
301
|
+
await indexer.index();
|
|
302
|
+
indexer.close();
|
|
303
|
+
const search = new SearchEngine(ctx.projectRoot);
|
|
304
|
+
// Should find both api.ts and auth.ts in services/
|
|
305
|
+
const apiResults = await search.search('API request fetch endpoint');
|
|
306
|
+
expect(apiResults.results.some(r => r.filepath.includes('services/api.ts'))).toBe(true);
|
|
307
|
+
const authResults = await search.search('authentication token login');
|
|
308
|
+
expect(authResults.results.some(r => r.filepath.includes('services/auth.ts'))).toBe(true);
|
|
309
|
+
search.close();
|
|
310
|
+
}, 60000);
|
|
311
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for exhaustive search mode.
|
|
3
|
+
*
|
|
4
|
+
* Tests the exhaustive mode for refactoring tasks:
|
|
5
|
+
* - Returns totalMatches count
|
|
6
|
+
* - Returns more results than default limit
|
|
7
|
+
* - Works with filters
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
10
|
+
import { Indexer } from '../indexer/indexer.js';
|
|
11
|
+
import { SearchEngine } from '../search/index.js';
|
|
12
|
+
import { copyFixtureToTemp } from './helpers.js';
|
|
13
|
+
describe('Exhaustive Mode', () => {
|
|
14
|
+
let ctx;
|
|
15
|
+
let search;
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
// Setup once - index the codebase
|
|
18
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
19
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
20
|
+
await indexer.index();
|
|
21
|
+
indexer.close();
|
|
22
|
+
search = new SearchEngine(ctx.projectRoot);
|
|
23
|
+
}, 120000);
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
search.close();
|
|
26
|
+
await ctx.cleanup();
|
|
27
|
+
});
|
|
28
|
+
it('returns totalMatches count when exhaustive is true', async () => {
|
|
29
|
+
const results = await search.search('function', { exhaustive: true });
|
|
30
|
+
expect(results.totalMatches).toBeDefined();
|
|
31
|
+
expect(typeof results.totalMatches).toBe('number');
|
|
32
|
+
expect(results.totalMatches).toBeGreaterThan(0);
|
|
33
|
+
// totalMatches should equal results length in exhaustive mode
|
|
34
|
+
expect(results.totalMatches).toBe(results.results.length);
|
|
35
|
+
}, 60000);
|
|
36
|
+
it('does not return totalMatches when exhaustive is false', async () => {
|
|
37
|
+
const results = await search.search('function', { exhaustive: false });
|
|
38
|
+
expect(results.totalMatches).toBeUndefined();
|
|
39
|
+
}, 60000);
|
|
40
|
+
it('returns more results than default limit', async () => {
|
|
41
|
+
// Default limit is 10
|
|
42
|
+
const normalResults = await search.search('function', { limit: 5 });
|
|
43
|
+
const exhaustiveResults = await search.search('function', {
|
|
44
|
+
exhaustive: true,
|
|
45
|
+
});
|
|
46
|
+
// Exhaustive should return at least as many results
|
|
47
|
+
expect(exhaustiveResults.results.length).toBeGreaterThanOrEqual(normalResults.results.length);
|
|
48
|
+
}, 60000);
|
|
49
|
+
it('works with semantic mode', async () => {
|
|
50
|
+
const results = await search.search('data processing', {
|
|
51
|
+
mode: 'semantic',
|
|
52
|
+
exhaustive: true,
|
|
53
|
+
});
|
|
54
|
+
expect(results.totalMatches).toBeDefined();
|
|
55
|
+
expect(results.searchType).toBe('semantic');
|
|
56
|
+
}, 60000);
|
|
57
|
+
it('works with exact mode', async () => {
|
|
58
|
+
const results = await search.search('function', {
|
|
59
|
+
mode: 'exact',
|
|
60
|
+
exhaustive: true,
|
|
61
|
+
});
|
|
62
|
+
expect(results.totalMatches).toBeDefined();
|
|
63
|
+
expect(results.searchType).toBe('exact');
|
|
64
|
+
}, 60000);
|
|
65
|
+
it('works with filters', async () => {
|
|
66
|
+
const results = await search.search('function', {
|
|
67
|
+
exhaustive: true,
|
|
68
|
+
filters: { extension: ['.ts'] },
|
|
69
|
+
});
|
|
70
|
+
expect(results.totalMatches).toBeDefined();
|
|
71
|
+
// All results should be TypeScript
|
|
72
|
+
expect(results.results.every(r => r.filepath.endsWith('.ts'))).toBe(true);
|
|
73
|
+
}, 60000);
|
|
74
|
+
it('respects minScore threshold', async () => {
|
|
75
|
+
const allResults = await search.search('function', { exhaustive: true });
|
|
76
|
+
const filteredResults = await search.search('function', {
|
|
77
|
+
exhaustive: true,
|
|
78
|
+
minScore: 0.5,
|
|
79
|
+
});
|
|
80
|
+
// Filtered should have equal or fewer results
|
|
81
|
+
expect(filteredResults.results.length).toBeLessThanOrEqual(allResults.results.length);
|
|
82
|
+
// All filtered results should have score >= 0.5
|
|
83
|
+
filteredResults.results.forEach(r => {
|
|
84
|
+
expect(r.score).toBeGreaterThanOrEqual(0.5);
|
|
85
|
+
});
|
|
86
|
+
}, 60000);
|
|
87
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for search filters.
|
|
3
|
+
*
|
|
4
|
+
* Tests the transparent, AI-controlled filter system:
|
|
5
|
+
* - Path filters: pathPrefix, pathContains, pathNotContains
|
|
6
|
+
* - Type filters: type, extension
|
|
7
|
+
* - Metadata filters: isExported, decoratorContains, hasDocstring
|
|
8
|
+
* - Filter combinations
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for search filters.
|
|
3
|
+
*
|
|
4
|
+
* Tests the transparent, AI-controlled filter system:
|
|
5
|
+
* - Path filters: pathPrefix, pathContains, pathNotContains
|
|
6
|
+
* - Type filters: type, extension
|
|
7
|
+
* - Metadata filters: isExported, decoratorContains, hasDocstring
|
|
8
|
+
* - Filter combinations
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
11
|
+
import { Indexer } from '../indexer/indexer.js';
|
|
12
|
+
import { SearchEngine } from '../search/index.js';
|
|
13
|
+
import { copyFixtureToTemp } from './helpers.js';
|
|
14
|
+
describe('Search Filters', () => {
|
|
15
|
+
let ctx;
|
|
16
|
+
let search;
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
// Setup once - index the codebase
|
|
19
|
+
ctx = await copyFixtureToTemp('codebase');
|
|
20
|
+
const indexer = new Indexer(ctx.projectRoot);
|
|
21
|
+
await indexer.index();
|
|
22
|
+
indexer.close();
|
|
23
|
+
search = new SearchEngine(ctx.projectRoot);
|
|
24
|
+
}, 120000);
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
search.close();
|
|
27
|
+
await ctx.cleanup();
|
|
28
|
+
});
|
|
29
|
+
describe('path filters', () => {
|
|
30
|
+
it('filters by pathPrefix', async () => {
|
|
31
|
+
const results = await search.search('function', {
|
|
32
|
+
filters: { pathPrefix: 'src/' },
|
|
33
|
+
});
|
|
34
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
35
|
+
// All results should be in src/ directory
|
|
36
|
+
expect(results.results.every(r => r.filepath.startsWith('src/'))).toBe(true);
|
|
37
|
+
}, 60000);
|
|
38
|
+
it('filters by pathPrefix to specific subdirectory', async () => {
|
|
39
|
+
const results = await search.search('function', {
|
|
40
|
+
filters: { pathPrefix: 'src/api/' },
|
|
41
|
+
});
|
|
42
|
+
// All results should be in src/api/
|
|
43
|
+
results.results.forEach(r => {
|
|
44
|
+
expect(r.filepath.startsWith('src/api/')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
}, 60000);
|
|
47
|
+
it('filters by pathContains', async () => {
|
|
48
|
+
const results = await search.search('function', {
|
|
49
|
+
filters: { pathContains: ['services'] },
|
|
50
|
+
});
|
|
51
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
52
|
+
// All results should have 'services' in path
|
|
53
|
+
expect(results.results.every(r => r.filepath.includes('services'))).toBe(true);
|
|
54
|
+
}, 60000);
|
|
55
|
+
it('filters by pathContains with multiple strings (AND)', async () => {
|
|
56
|
+
const results = await search.search('function', {
|
|
57
|
+
filters: { pathContains: ['src', 'utils'] },
|
|
58
|
+
});
|
|
59
|
+
// All results must contain BOTH strings
|
|
60
|
+
results.results.forEach(r => {
|
|
61
|
+
expect(r.filepath.includes('src')).toBe(true);
|
|
62
|
+
expect(r.filepath.includes('utils')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
}, 60000);
|
|
65
|
+
it('excludes paths with pathNotContains', async () => {
|
|
66
|
+
const results = await search.search('function', {
|
|
67
|
+
filters: { pathNotContains: ['__tests__'] },
|
|
68
|
+
});
|
|
69
|
+
// No results should be in __tests__ directory
|
|
70
|
+
expect(results.results.every(r => !r.filepath.includes('__tests__'))).toBe(true);
|
|
71
|
+
}, 60000);
|
|
72
|
+
it('excludes multiple patterns with pathNotContains', async () => {
|
|
73
|
+
const results = await search.search('function', {
|
|
74
|
+
filters: { pathNotContains: ['__tests__', '.test.', '.spec.'] },
|
|
75
|
+
});
|
|
76
|
+
// No results should match any exclusion pattern
|
|
77
|
+
results.results.forEach(r => {
|
|
78
|
+
expect(r.filepath.includes('__tests__')).toBe(false);
|
|
79
|
+
expect(r.filepath.includes('.test.')).toBe(false);
|
|
80
|
+
expect(r.filepath.includes('.spec.')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
}, 60000);
|
|
83
|
+
});
|
|
84
|
+
describe('type filters', () => {
|
|
85
|
+
it('filters by function type', async () => {
|
|
86
|
+
const results = await search.search('data', {
|
|
87
|
+
filters: { type: ['function'] },
|
|
88
|
+
});
|
|
89
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
90
|
+
// All results should be functions
|
|
91
|
+
expect(results.results.every(r => r.type === 'function')).toBe(true);
|
|
92
|
+
}, 60000);
|
|
93
|
+
it('filters by class type', async () => {
|
|
94
|
+
const results = await search.search('service', {
|
|
95
|
+
filters: { type: ['class'] },
|
|
96
|
+
});
|
|
97
|
+
// All results should be classes
|
|
98
|
+
results.results.forEach(r => {
|
|
99
|
+
expect(r.type).toBe('class');
|
|
100
|
+
});
|
|
101
|
+
}, 60000);
|
|
102
|
+
it('filters by method type', async () => {
|
|
103
|
+
const results = await search.search('get', {
|
|
104
|
+
filters: { type: ['method'] },
|
|
105
|
+
});
|
|
106
|
+
// All results should be methods
|
|
107
|
+
results.results.forEach(r => {
|
|
108
|
+
expect(r.type).toBe('method');
|
|
109
|
+
});
|
|
110
|
+
}, 60000);
|
|
111
|
+
it('filters by multiple types (OR)', async () => {
|
|
112
|
+
const results = await search.search('user', {
|
|
113
|
+
filters: { type: ['function', 'class'] },
|
|
114
|
+
});
|
|
115
|
+
// All results should be either function or class
|
|
116
|
+
results.results.forEach(r => {
|
|
117
|
+
expect(['function', 'class']).toContain(r.type);
|
|
118
|
+
});
|
|
119
|
+
}, 60000);
|
|
120
|
+
});
|
|
121
|
+
describe('extension filters', () => {
|
|
122
|
+
it('filters by .py extension', async () => {
|
|
123
|
+
const results = await search.search('function', {
|
|
124
|
+
filters: { extension: ['.py'] },
|
|
125
|
+
});
|
|
126
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
127
|
+
// All results should be Python files
|
|
128
|
+
expect(results.results.every(r => r.filepath.endsWith('.py'))).toBe(true);
|
|
129
|
+
}, 60000);
|
|
130
|
+
it('filters by .ts extension', async () => {
|
|
131
|
+
const results = await search.search('function', {
|
|
132
|
+
filters: { extension: ['.ts'] },
|
|
133
|
+
});
|
|
134
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
135
|
+
// All results should be TypeScript files
|
|
136
|
+
expect(results.results.every(r => r.filepath.endsWith('.ts'))).toBe(true);
|
|
137
|
+
}, 60000);
|
|
138
|
+
it('filters by multiple extensions (OR)', async () => {
|
|
139
|
+
const results = await search.search('function', {
|
|
140
|
+
filters: { extension: ['.ts', '.tsx'] },
|
|
141
|
+
});
|
|
142
|
+
// All results should be .ts or .tsx files
|
|
143
|
+
results.results.forEach(r => {
|
|
144
|
+
expect(r.filepath.endsWith('.ts') || r.filepath.endsWith('.tsx')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
}, 60000);
|
|
147
|
+
it('filters by .js extension', async () => {
|
|
148
|
+
const results = await search.search('format', {
|
|
149
|
+
filters: { extension: ['.js'] },
|
|
150
|
+
});
|
|
151
|
+
// All results should be JavaScript files
|
|
152
|
+
results.results.forEach(r => {
|
|
153
|
+
expect(r.filepath.endsWith('.js')).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
}, 60000);
|
|
156
|
+
});
|
|
157
|
+
describe('metadata filters', () => {
|
|
158
|
+
it('filters by isExported true', async () => {
|
|
159
|
+
const results = await search.search('function', {
|
|
160
|
+
filters: { isExported: true },
|
|
161
|
+
});
|
|
162
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
163
|
+
// All results should be exported
|
|
164
|
+
expect(results.results.every(r => r.isExported === true)).toBe(true);
|
|
165
|
+
}, 60000);
|
|
166
|
+
it('filters by isExported false', async () => {
|
|
167
|
+
const results = await search.search('helper internal', {
|
|
168
|
+
filters: { isExported: false },
|
|
169
|
+
});
|
|
170
|
+
// All results should NOT be exported
|
|
171
|
+
results.results.forEach(r => {
|
|
172
|
+
expect(r.isExported).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
}, 60000);
|
|
175
|
+
it('filters by decoratorContains', async () => {
|
|
176
|
+
const results = await search.search('function', {
|
|
177
|
+
filters: { decoratorContains: 'log' },
|
|
178
|
+
});
|
|
179
|
+
// Should find decorated Python functions
|
|
180
|
+
// Results may be empty if no decorated functions match, that's ok
|
|
181
|
+
if (results.results.length > 0) {
|
|
182
|
+
expect(results.results.some(r => r.filepath.includes('decorators.py'))).toBe(true);
|
|
183
|
+
}
|
|
184
|
+
}, 60000);
|
|
185
|
+
it('filters by hasDocstring true', async () => {
|
|
186
|
+
const results = await search.search('function', {
|
|
187
|
+
filters: { hasDocstring: true },
|
|
188
|
+
});
|
|
189
|
+
// Results should be documented code
|
|
190
|
+
// Most fixture files have JSDoc/docstrings
|
|
191
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
192
|
+
}, 60000);
|
|
193
|
+
});
|
|
194
|
+
describe('filter combinations', () => {
|
|
195
|
+
it('combines pathPrefix + type', async () => {
|
|
196
|
+
const results = await search.search('data', {
|
|
197
|
+
filters: {
|
|
198
|
+
pathPrefix: 'src/',
|
|
199
|
+
type: ['function'],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
// All results should be functions in src/
|
|
203
|
+
results.results.forEach(r => {
|
|
204
|
+
expect(r.filepath.startsWith('src/')).toBe(true);
|
|
205
|
+
expect(r.type).toBe('function');
|
|
206
|
+
});
|
|
207
|
+
}, 60000);
|
|
208
|
+
it('combines pathNotContains + isExported', async () => {
|
|
209
|
+
const results = await search.search('user', {
|
|
210
|
+
filters: {
|
|
211
|
+
pathNotContains: ['__tests__'],
|
|
212
|
+
isExported: true,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
// All results should be exported and NOT in tests
|
|
216
|
+
results.results.forEach(r => {
|
|
217
|
+
expect(r.filepath.includes('__tests__')).toBe(false);
|
|
218
|
+
expect(r.isExported).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
}, 60000);
|
|
221
|
+
it('combines extension + type', async () => {
|
|
222
|
+
const results = await search.search('process', {
|
|
223
|
+
filters: {
|
|
224
|
+
extension: ['.ts'],
|
|
225
|
+
type: ['function'],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
// All results should be TypeScript functions
|
|
229
|
+
results.results.forEach(r => {
|
|
230
|
+
expect(r.filepath.endsWith('.ts')).toBe(true);
|
|
231
|
+
expect(r.type).toBe('function');
|
|
232
|
+
});
|
|
233
|
+
}, 60000);
|
|
234
|
+
it('combines pathPrefix + pathNotContains + type', async () => {
|
|
235
|
+
const results = await search.search('function', {
|
|
236
|
+
filters: {
|
|
237
|
+
pathPrefix: 'src/',
|
|
238
|
+
pathNotContains: ['__tests__'],
|
|
239
|
+
type: ['function', 'method'],
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
// Complex filter combination
|
|
243
|
+
results.results.forEach(r => {
|
|
244
|
+
expect(r.filepath.startsWith('src/')).toBe(true);
|
|
245
|
+
expect(r.filepath.includes('__tests__')).toBe(false);
|
|
246
|
+
expect(['function', 'method']).toContain(r.type);
|
|
247
|
+
});
|
|
248
|
+
}, 60000);
|
|
249
|
+
});
|
|
250
|
+
});
|