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.
Files changed (151) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +219 -0
  3. package/dist/cli/__tests__/mcp-setup.test.d.ts +6 -0
  4. package/dist/cli/__tests__/mcp-setup.test.js +597 -0
  5. package/dist/cli/app.d.ts +2 -0
  6. package/dist/cli/app.js +238 -0
  7. package/dist/cli/commands/handlers.d.ts +57 -0
  8. package/dist/cli/commands/handlers.js +231 -0
  9. package/dist/cli/commands/index.d.ts +2 -0
  10. package/dist/cli/commands/index.js +2 -0
  11. package/dist/cli/commands/mcp-setup.d.ts +107 -0
  12. package/dist/cli/commands/mcp-setup.js +509 -0
  13. package/dist/cli/commands/useRagCommands.d.ts +23 -0
  14. package/dist/cli/commands/useRagCommands.js +180 -0
  15. package/dist/cli/components/CleanWizard.d.ts +17 -0
  16. package/dist/cli/components/CleanWizard.js +169 -0
  17. package/dist/cli/components/InitWizard.d.ts +20 -0
  18. package/dist/cli/components/InitWizard.js +370 -0
  19. package/dist/cli/components/McpSetupWizard.d.ts +37 -0
  20. package/dist/cli/components/McpSetupWizard.js +387 -0
  21. package/dist/cli/components/SearchResultsDisplay.d.ts +13 -0
  22. package/dist/cli/components/SearchResultsDisplay.js +130 -0
  23. package/dist/cli/components/WelcomeBanner.d.ts +10 -0
  24. package/dist/cli/components/WelcomeBanner.js +26 -0
  25. package/dist/cli/components/index.d.ts +1 -0
  26. package/dist/cli/components/index.js +1 -0
  27. package/dist/cli/data/mcp-editors.d.ts +80 -0
  28. package/dist/cli/data/mcp-editors.js +270 -0
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.js +26 -0
  31. package/dist/cli-bundle.cjs +5269 -0
  32. package/dist/common/commands/terminalSetup.d.ts +2 -0
  33. package/dist/common/commands/terminalSetup.js +144 -0
  34. package/dist/common/components/CommandSuggestions.d.ts +9 -0
  35. package/dist/common/components/CommandSuggestions.js +20 -0
  36. package/dist/common/components/StaticWithResize.d.ts +23 -0
  37. package/dist/common/components/StaticWithResize.js +62 -0
  38. package/dist/common/components/StatusBar.d.ts +8 -0
  39. package/dist/common/components/StatusBar.js +64 -0
  40. package/dist/common/components/TextInput.d.ts +12 -0
  41. package/dist/common/components/TextInput.js +239 -0
  42. package/dist/common/components/index.d.ts +3 -0
  43. package/dist/common/components/index.js +3 -0
  44. package/dist/common/hooks/index.d.ts +4 -0
  45. package/dist/common/hooks/index.js +4 -0
  46. package/dist/common/hooks/useCommandHistory.d.ts +7 -0
  47. package/dist/common/hooks/useCommandHistory.js +51 -0
  48. package/dist/common/hooks/useCtrlC.d.ts +9 -0
  49. package/dist/common/hooks/useCtrlC.js +40 -0
  50. package/dist/common/hooks/useKittyKeyboard.d.ts +10 -0
  51. package/dist/common/hooks/useKittyKeyboard.js +26 -0
  52. package/dist/common/hooks/useStaticOutputBuffer.d.ts +31 -0
  53. package/dist/common/hooks/useStaticOutputBuffer.js +58 -0
  54. package/dist/common/hooks/useTerminalResize.d.ts +28 -0
  55. package/dist/common/hooks/useTerminalResize.js +51 -0
  56. package/dist/common/hooks/useTextBuffer.d.ts +13 -0
  57. package/dist/common/hooks/useTextBuffer.js +165 -0
  58. package/dist/common/index.d.ts +13 -0
  59. package/dist/common/index.js +17 -0
  60. package/dist/common/types.d.ts +162 -0
  61. package/dist/common/types.js +1 -0
  62. package/dist/mcp/index.d.ts +12 -0
  63. package/dist/mcp/index.js +66 -0
  64. package/dist/mcp/server.d.ts +25 -0
  65. package/dist/mcp/server.js +837 -0
  66. package/dist/mcp/watcher.d.ts +86 -0
  67. package/dist/mcp/watcher.js +334 -0
  68. package/dist/rag/__tests__/grammar-smoke.test.d.ts +9 -0
  69. package/dist/rag/__tests__/grammar-smoke.test.js +161 -0
  70. package/dist/rag/__tests__/helpers.d.ts +30 -0
  71. package/dist/rag/__tests__/helpers.js +67 -0
  72. package/dist/rag/__tests__/merkle.test.d.ts +5 -0
  73. package/dist/rag/__tests__/merkle.test.js +161 -0
  74. package/dist/rag/__tests__/metadata-extraction.test.d.ts +10 -0
  75. package/dist/rag/__tests__/metadata-extraction.test.js +202 -0
  76. package/dist/rag/__tests__/multi-language.test.d.ts +13 -0
  77. package/dist/rag/__tests__/multi-language.test.js +535 -0
  78. package/dist/rag/__tests__/rag.test.d.ts +10 -0
  79. package/dist/rag/__tests__/rag.test.js +311 -0
  80. package/dist/rag/__tests__/search-exhaustive.test.d.ts +9 -0
  81. package/dist/rag/__tests__/search-exhaustive.test.js +87 -0
  82. package/dist/rag/__tests__/search-filters.test.d.ts +10 -0
  83. package/dist/rag/__tests__/search-filters.test.js +250 -0
  84. package/dist/rag/__tests__/search-modes.test.d.ts +8 -0
  85. package/dist/rag/__tests__/search-modes.test.js +133 -0
  86. package/dist/rag/config/index.d.ts +61 -0
  87. package/dist/rag/config/index.js +111 -0
  88. package/dist/rag/constants.d.ts +41 -0
  89. package/dist/rag/constants.js +57 -0
  90. package/dist/rag/embeddings/fastembed.d.ts +62 -0
  91. package/dist/rag/embeddings/fastembed.js +124 -0
  92. package/dist/rag/embeddings/gemini.d.ts +26 -0
  93. package/dist/rag/embeddings/gemini.js +116 -0
  94. package/dist/rag/embeddings/index.d.ts +10 -0
  95. package/dist/rag/embeddings/index.js +9 -0
  96. package/dist/rag/embeddings/local-4b.d.ts +28 -0
  97. package/dist/rag/embeddings/local-4b.js +51 -0
  98. package/dist/rag/embeddings/local.d.ts +29 -0
  99. package/dist/rag/embeddings/local.js +119 -0
  100. package/dist/rag/embeddings/mistral.d.ts +22 -0
  101. package/dist/rag/embeddings/mistral.js +85 -0
  102. package/dist/rag/embeddings/openai.d.ts +22 -0
  103. package/dist/rag/embeddings/openai.js +85 -0
  104. package/dist/rag/embeddings/types.d.ts +37 -0
  105. package/dist/rag/embeddings/types.js +1 -0
  106. package/dist/rag/gitignore/index.d.ts +57 -0
  107. package/dist/rag/gitignore/index.js +178 -0
  108. package/dist/rag/index.d.ts +15 -0
  109. package/dist/rag/index.js +25 -0
  110. package/dist/rag/indexer/chunker.d.ts +129 -0
  111. package/dist/rag/indexer/chunker.js +1352 -0
  112. package/dist/rag/indexer/index.d.ts +6 -0
  113. package/dist/rag/indexer/index.js +6 -0
  114. package/dist/rag/indexer/indexer.d.ts +73 -0
  115. package/dist/rag/indexer/indexer.js +356 -0
  116. package/dist/rag/indexer/types.d.ts +68 -0
  117. package/dist/rag/indexer/types.js +47 -0
  118. package/dist/rag/logger/index.d.ts +20 -0
  119. package/dist/rag/logger/index.js +75 -0
  120. package/dist/rag/manifest/index.d.ts +50 -0
  121. package/dist/rag/manifest/index.js +97 -0
  122. package/dist/rag/merkle/diff.d.ts +26 -0
  123. package/dist/rag/merkle/diff.js +95 -0
  124. package/dist/rag/merkle/hash.d.ts +34 -0
  125. package/dist/rag/merkle/hash.js +165 -0
  126. package/dist/rag/merkle/index.d.ts +68 -0
  127. package/dist/rag/merkle/index.js +298 -0
  128. package/dist/rag/merkle/node.d.ts +51 -0
  129. package/dist/rag/merkle/node.js +69 -0
  130. package/dist/rag/search/filters.d.ts +21 -0
  131. package/dist/rag/search/filters.js +100 -0
  132. package/dist/rag/search/fts.d.ts +32 -0
  133. package/dist/rag/search/fts.js +61 -0
  134. package/dist/rag/search/hybrid.d.ts +17 -0
  135. package/dist/rag/search/hybrid.js +58 -0
  136. package/dist/rag/search/index.d.ts +89 -0
  137. package/dist/rag/search/index.js +367 -0
  138. package/dist/rag/search/types.d.ts +130 -0
  139. package/dist/rag/search/types.js +4 -0
  140. package/dist/rag/search/vector.d.ts +25 -0
  141. package/dist/rag/search/vector.js +44 -0
  142. package/dist/rag/storage/index.d.ts +92 -0
  143. package/dist/rag/storage/index.js +287 -0
  144. package/dist/rag/storage/lancedb-native.d.ts +7 -0
  145. package/dist/rag/storage/lancedb-native.js +10 -0
  146. package/dist/rag/storage/schema.d.ts +23 -0
  147. package/dist/rag/storage/schema.js +50 -0
  148. package/dist/rag/storage/types.d.ts +100 -0
  149. package/dist/rag/storage/types.js +68 -0
  150. package/package.json +67 -0
  151. 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,9 @@
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
+ export {};
@@ -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
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tests for search modes: definition and similar.
3
+ *
4
+ * Tests the new search modes added in Phase 2:
5
+ * - definition: Direct metadata lookup by symbol name
6
+ * - similar: Vector search with code snippet as query
7
+ */
8
+ export {};