@mhalder/qdrant-mcp-server 1.4.0 → 1.5.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 (82) hide show
  1. package/.codecov.yml +16 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +236 -9
  4. package/build/code/chunker/base.d.ts +19 -0
  5. package/build/code/chunker/base.d.ts.map +1 -0
  6. package/build/code/chunker/base.js +5 -0
  7. package/build/code/chunker/base.js.map +1 -0
  8. package/build/code/chunker/character-chunker.d.ts +22 -0
  9. package/build/code/chunker/character-chunker.d.ts.map +1 -0
  10. package/build/code/chunker/character-chunker.js +111 -0
  11. package/build/code/chunker/character-chunker.js.map +1 -0
  12. package/build/code/chunker/tree-sitter-chunker.d.ts +29 -0
  13. package/build/code/chunker/tree-sitter-chunker.d.ts.map +1 -0
  14. package/build/code/chunker/tree-sitter-chunker.js +213 -0
  15. package/build/code/chunker/tree-sitter-chunker.js.map +1 -0
  16. package/build/code/config.d.ts +11 -0
  17. package/build/code/config.d.ts.map +1 -0
  18. package/build/code/config.js +145 -0
  19. package/build/code/config.js.map +1 -0
  20. package/build/code/indexer.d.ts +42 -0
  21. package/build/code/indexer.d.ts.map +1 -0
  22. package/build/code/indexer.js +508 -0
  23. package/build/code/indexer.js.map +1 -0
  24. package/build/code/metadata.d.ts +32 -0
  25. package/build/code/metadata.d.ts.map +1 -0
  26. package/build/code/metadata.js +128 -0
  27. package/build/code/metadata.js.map +1 -0
  28. package/build/code/scanner.d.ts +35 -0
  29. package/build/code/scanner.d.ts.map +1 -0
  30. package/build/code/scanner.js +108 -0
  31. package/build/code/scanner.js.map +1 -0
  32. package/build/code/sync/merkle.d.ts +45 -0
  33. package/build/code/sync/merkle.d.ts.map +1 -0
  34. package/build/code/sync/merkle.js +116 -0
  35. package/build/code/sync/merkle.js.map +1 -0
  36. package/build/code/sync/snapshot.d.ts +41 -0
  37. package/build/code/sync/snapshot.d.ts.map +1 -0
  38. package/build/code/sync/snapshot.js +91 -0
  39. package/build/code/sync/snapshot.js.map +1 -0
  40. package/build/code/sync/synchronizer.d.ts +53 -0
  41. package/build/code/sync/synchronizer.d.ts.map +1 -0
  42. package/build/code/sync/synchronizer.js +132 -0
  43. package/build/code/sync/synchronizer.js.map +1 -0
  44. package/build/code/types.d.ts +98 -0
  45. package/build/code/types.d.ts.map +1 -0
  46. package/build/code/types.js +5 -0
  47. package/build/code/types.js.map +1 -0
  48. package/build/index.js +250 -0
  49. package/build/index.js.map +1 -1
  50. package/examples/code-search/README.md +271 -0
  51. package/package.json +13 -1
  52. package/src/code/chunker/base.ts +22 -0
  53. package/src/code/chunker/character-chunker.ts +131 -0
  54. package/src/code/chunker/tree-sitter-chunker.ts +250 -0
  55. package/src/code/config.ts +156 -0
  56. package/src/code/indexer.ts +613 -0
  57. package/src/code/metadata.ts +153 -0
  58. package/src/code/scanner.ts +124 -0
  59. package/src/code/sync/merkle.ts +136 -0
  60. package/src/code/sync/snapshot.ts +110 -0
  61. package/src/code/sync/synchronizer.ts +154 -0
  62. package/src/code/types.ts +117 -0
  63. package/src/index.ts +296 -0
  64. package/tests/code/chunker/character-chunker.test.ts +141 -0
  65. package/tests/code/chunker/tree-sitter-chunker.test.ts +275 -0
  66. package/tests/code/fixtures/sample-py/calculator.py +32 -0
  67. package/tests/code/fixtures/sample-ts/async-operations.ts +120 -0
  68. package/tests/code/fixtures/sample-ts/auth.ts +31 -0
  69. package/tests/code/fixtures/sample-ts/config.ts +52 -0
  70. package/tests/code/fixtures/sample-ts/database.ts +50 -0
  71. package/tests/code/fixtures/sample-ts/index.ts +39 -0
  72. package/tests/code/fixtures/sample-ts/types-advanced.ts +132 -0
  73. package/tests/code/fixtures/sample-ts/utils.ts +105 -0
  74. package/tests/code/fixtures/sample-ts/validator.ts +169 -0
  75. package/tests/code/indexer.test.ts +828 -0
  76. package/tests/code/integration.test.ts +708 -0
  77. package/tests/code/metadata.test.ts +457 -0
  78. package/tests/code/scanner.test.ts +131 -0
  79. package/tests/code/sync/merkle.test.ts +406 -0
  80. package/tests/code/sync/snapshot.test.ts +360 -0
  81. package/tests/code/sync/synchronizer.test.ts +501 -0
  82. package/vitest.config.ts +1 -0
@@ -0,0 +1,708 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { CodeIndexer } from "../../src/code/indexer.js";
6
+ import type { CodeConfig } from "../../src/code/types.js";
7
+ import type { EmbeddingProvider } from "../../src/embeddings/base.js";
8
+ import type { QdrantManager } from "../../src/qdrant/client.js";
9
+
10
+ // Mock implementations (same as indexer.test.ts)
11
+ class MockQdrantManager implements Partial<QdrantManager> {
12
+ private collections = new Map<string, any>();
13
+ private points = new Map<string, any[]>();
14
+
15
+ async collectionExists(name: string): Promise<boolean> {
16
+ return this.collections.has(name);
17
+ }
18
+
19
+ async createCollection(
20
+ name: string,
21
+ vectorSize: number,
22
+ distance: string,
23
+ enableHybrid?: boolean
24
+ ): Promise<void> {
25
+ this.collections.set(name, {
26
+ vectorSize,
27
+ distance,
28
+ hybridEnabled: enableHybrid || false,
29
+ });
30
+ this.points.set(name, []);
31
+ }
32
+
33
+ async deleteCollection(name: string): Promise<void> {
34
+ this.collections.delete(name);
35
+ this.points.delete(name);
36
+ }
37
+
38
+ async addPoints(collectionName: string, points: any[]): Promise<void> {
39
+ const existing = this.points.get(collectionName) || [];
40
+ this.points.set(collectionName, [...existing, ...points]);
41
+ }
42
+
43
+ async addPointsWithSparse(collectionName: string, points: any[]): Promise<void> {
44
+ await this.addPoints(collectionName, points);
45
+ }
46
+
47
+ async search(
48
+ collectionName: string,
49
+ _vector: number[],
50
+ limit: number,
51
+ filter?: any
52
+ ): Promise<any[]> {
53
+ const points = this.points.get(collectionName) || [];
54
+ let filtered = points;
55
+
56
+ // Simple filtering implementation
57
+ if (filter?.must) {
58
+ for (const condition of filter.must) {
59
+ if (condition.key === "fileExtension") {
60
+ filtered = filtered.filter((p) => condition.match.any.includes(p.payload.fileExtension));
61
+ }
62
+ }
63
+ }
64
+
65
+ return filtered.slice(0, limit).map((p, idx) => ({
66
+ id: p.id,
67
+ score: 0.9 - idx * 0.05,
68
+ payload: p.payload,
69
+ }));
70
+ }
71
+
72
+ async hybridSearch(
73
+ collectionName: string,
74
+ vector: number[],
75
+ _sparseVector: any,
76
+ limit: number,
77
+ filter?: any
78
+ ): Promise<any[]> {
79
+ // Hybrid search returns similar results with slight boost
80
+ const results = await this.search(collectionName, vector, limit, filter);
81
+ return results.map((r) => ({ ...r, score: Math.min(r.score + 0.05, 1.0) }));
82
+ }
83
+
84
+ async getCollectionInfo(name: string): Promise<any> {
85
+ const collection = this.collections.get(name);
86
+ const points = this.points.get(name) || [];
87
+ return {
88
+ pointsCount: points.length,
89
+ hybridEnabled: collection?.hybridEnabled || false,
90
+ vectorSize: collection?.vectorSize || 384,
91
+ };
92
+ }
93
+ }
94
+
95
+ class MockEmbeddingProvider implements EmbeddingProvider {
96
+ getDimensions(): number {
97
+ return 384;
98
+ }
99
+
100
+ async embed(text: string): Promise<{ embedding: number[] }> {
101
+ // Simple hash-based mock embedding
102
+ const hash = text.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
103
+ const base = (hash % 100) / 100;
104
+ return { embedding: new Array(384).fill(base) };
105
+ }
106
+
107
+ async embedBatch(texts: string[]): Promise<Array<{ embedding: number[] }>> {
108
+ return Promise.all(texts.map((text) => this.embed(text)));
109
+ }
110
+ }
111
+
112
+ describe("CodeIndexer Integration Tests", () => {
113
+ let indexer: CodeIndexer;
114
+ let qdrant: MockQdrantManager;
115
+ let embeddings: MockEmbeddingProvider;
116
+ let config: CodeConfig;
117
+ let tempDir: string;
118
+ let codebaseDir: string;
119
+
120
+ beforeEach(async () => {
121
+ tempDir = join(
122
+ tmpdir(),
123
+ `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`
124
+ );
125
+ codebaseDir = join(tempDir, "codebase");
126
+ await fs.mkdir(codebaseDir, { recursive: true });
127
+
128
+ qdrant = new MockQdrantManager() as any;
129
+ embeddings = new MockEmbeddingProvider();
130
+ config = {
131
+ chunkSize: 500,
132
+ chunkOverlap: 50,
133
+ enableASTChunking: true,
134
+ supportedExtensions: [".ts", ".js", ".py", ".go"],
135
+ ignorePatterns: ["node_modules/**", "dist/**", "*.test.*"],
136
+ batchSize: 10,
137
+ defaultSearchLimit: 5,
138
+ enableHybridSearch: false,
139
+ };
140
+
141
+ indexer = new CodeIndexer(qdrant as any, embeddings, config);
142
+ });
143
+
144
+ afterEach(async () => {
145
+ try {
146
+ await fs.rm(tempDir, { recursive: true, force: true });
147
+ } catch (_error) {
148
+ // Ignore cleanup errors
149
+ }
150
+ });
151
+
152
+ describe("Complete indexing workflow", () => {
153
+ it("should index, search, and retrieve results from a TypeScript project", async () => {
154
+ // Create a sample TypeScript project structure
155
+ await createTestFile(
156
+ codebaseDir,
157
+ "src/auth/login.ts",
158
+ `
159
+ export class AuthService {
160
+ async login(email: string, password: string) {
161
+ return { token: 'jwt-token' };
162
+ }
163
+ }
164
+ `
165
+ );
166
+
167
+ await createTestFile(
168
+ codebaseDir,
169
+ "src/auth/register.ts",
170
+ `
171
+ export class RegistrationService {
172
+ async register(user: User) {
173
+ return { id: '123', email: user.email };
174
+ }
175
+ }
176
+ `
177
+ );
178
+
179
+ await createTestFile(
180
+ codebaseDir,
181
+ "src/utils/validation.ts",
182
+ `
183
+ export function validateEmail(email: string): boolean {
184
+ return /^[^@]+@[^@]+\\.[^@]+$/.test(email);
185
+ }
186
+ `
187
+ );
188
+
189
+ // Index the codebase
190
+ const indexStats = await indexer.indexCodebase(codebaseDir);
191
+
192
+ expect(indexStats.filesScanned).toBe(3);
193
+ expect(indexStats.filesIndexed).toBe(3);
194
+ expect(indexStats.chunksCreated).toBeGreaterThan(0);
195
+ expect(indexStats.status).toBe("completed");
196
+
197
+ // Search for authentication-related code
198
+ const authResults = await indexer.searchCode(codebaseDir, "authentication login");
199
+
200
+ expect(authResults.length).toBeGreaterThan(0);
201
+ expect(authResults[0].language).toBe("typescript");
202
+
203
+ // Verify index status
204
+ const status = await indexer.getIndexStatus(codebaseDir);
205
+
206
+ expect(status.isIndexed).toBe(true);
207
+ expect(status.chunksCount).toBeGreaterThan(0);
208
+ });
209
+
210
+ it("should handle multi-language projects", async () => {
211
+ await createTestFile(
212
+ codebaseDir,
213
+ "server.ts",
214
+ `
215
+ import express from 'express';
216
+ const app = express();
217
+ app.listen(3000);
218
+ `
219
+ );
220
+
221
+ await createTestFile(
222
+ codebaseDir,
223
+ "client.js",
224
+ `
225
+ const API_URL = 'http://localhost:3000';
226
+ fetch(API_URL).then(res => res.json());
227
+ `
228
+ );
229
+
230
+ await createTestFile(
231
+ codebaseDir,
232
+ "utils.py",
233
+ `
234
+ def process_data(data):
235
+ return [x * 2 for x in data]
236
+ `
237
+ );
238
+
239
+ const stats = await indexer.indexCodebase(codebaseDir);
240
+
241
+ expect(stats.filesScanned).toBe(3);
242
+ expect(stats.filesIndexed).toBe(3);
243
+
244
+ // Search should find relevant code regardless of language
245
+ const results = await indexer.searchCode(codebaseDir, "process data");
246
+
247
+ expect(results.length).toBeGreaterThan(0);
248
+ });
249
+ });
250
+
251
+ describe("Incremental updates workflow", () => {
252
+ it("should detect and index only changed files", async () => {
253
+ // Initial indexing
254
+ await createTestFile(
255
+ codebaseDir,
256
+ "file1.ts",
257
+ `export const firstValue = 1;
258
+ console.log('First file loaded successfully');
259
+ function init(): string {
260
+ console.log('Initializing system');
261
+ return 'ready';
262
+ }`
263
+ );
264
+ await createTestFile(
265
+ codebaseDir,
266
+ "file2.ts",
267
+ `export const secondValue = 2;
268
+ console.log('Second file loaded successfully');
269
+ function start(): string {
270
+ console.log('Starting application');
271
+ return 'started';
272
+ }`
273
+ );
274
+
275
+ const initialStats = await indexer.indexCodebase(codebaseDir);
276
+ expect(initialStats.filesIndexed).toBe(2);
277
+
278
+ // Add a new file
279
+ await createTestFile(
280
+ codebaseDir,
281
+ "file3.ts",
282
+ `/**
283
+ * Process data and return result string
284
+ */
285
+ export function process(): string {
286
+ console.log('Processing data in third file');
287
+ const status = 'processed';
288
+ if (status) {
289
+ console.log('Status confirmed:', status);
290
+ }
291
+ return status;
292
+ }
293
+
294
+ export const thirdValue = 3;`
295
+ );
296
+
297
+ // Incremental update
298
+ const updateStats = await indexer.reindexChanges(codebaseDir);
299
+
300
+ expect(updateStats.filesAdded).toBe(1);
301
+ expect(updateStats.filesModified).toBe(0);
302
+ expect(updateStats.filesDeleted).toBe(0);
303
+
304
+ // Verify search includes new content
305
+ const results = await indexer.searchCode(codebaseDir, "third");
306
+ expect(results.length).toBeGreaterThan(0);
307
+ });
308
+
309
+ it("should handle file modifications", async () => {
310
+ await createTestFile(
311
+ codebaseDir,
312
+ "config.ts",
313
+ "export const DEBUG_MODE = false;\nconsole.log('Debug mode off');"
314
+ );
315
+
316
+ await indexer.indexCodebase(codebaseDir);
317
+
318
+ // Modify the file
319
+ await createTestFile(
320
+ codebaseDir,
321
+ "config.ts",
322
+ "export const DEBUG_MODE = true;\nconsole.log('Debug mode on');"
323
+ );
324
+
325
+ const updateStats = await indexer.reindexChanges(codebaseDir);
326
+
327
+ expect(updateStats.filesModified).toBe(1);
328
+ expect(updateStats.filesAdded).toBe(0);
329
+ });
330
+
331
+ it("should handle file deletions", async () => {
332
+ await createTestFile(
333
+ codebaseDir,
334
+ "temp.ts",
335
+ "export const tempValue = true;\nconsole.log('Temporary file created');\nfunction cleanup() { return null; }"
336
+ );
337
+ await createTestFile(
338
+ codebaseDir,
339
+ "keep.ts",
340
+ "export const keepValue = true;\nconsole.log('Permanent file stays');\nfunction maintain() { return true; }"
341
+ );
342
+
343
+ await indexer.indexCodebase(codebaseDir);
344
+
345
+ // Delete a file
346
+ await fs.unlink(join(codebaseDir, "temp.ts"));
347
+
348
+ const updateStats = await indexer.reindexChanges(codebaseDir);
349
+
350
+ expect(updateStats.filesDeleted).toBe(1);
351
+ expect(updateStats.filesAdded).toBe(0);
352
+ });
353
+
354
+ it("should handle mixed changes in one update", async () => {
355
+ // Initial state
356
+ await createTestFile(
357
+ codebaseDir,
358
+ "file1.ts",
359
+ "export const alpha = 1;\nconsole.log('Alpha file');"
360
+ );
361
+ await createTestFile(
362
+ codebaseDir,
363
+ "file2.ts",
364
+ "export const beta = 2;\nconsole.log('Beta file');"
365
+ );
366
+ await createTestFile(
367
+ codebaseDir,
368
+ "file3.ts",
369
+ "export const gamma = 3;\nconsole.log('Gamma file');"
370
+ );
371
+
372
+ await indexer.indexCodebase(codebaseDir);
373
+
374
+ // Mixed changes
375
+ await createTestFile(
376
+ codebaseDir,
377
+ "file1.ts",
378
+ "export const alpha = 100;\nconsole.log('Alpha modified');"
379
+ ); // Modified
380
+ await createTestFile(
381
+ codebaseDir,
382
+ "file4.ts",
383
+ "export const delta = 4;\nconsole.log('Delta file added');"
384
+ ); // Added
385
+ await fs.unlink(join(codebaseDir, "file3.ts")); // Deleted
386
+
387
+ const updateStats = await indexer.reindexChanges(codebaseDir);
388
+
389
+ expect(updateStats.filesAdded).toBe(1);
390
+ expect(updateStats.filesModified).toBe(1);
391
+ expect(updateStats.filesDeleted).toBe(1);
392
+ });
393
+ });
394
+
395
+ describe("Search filtering and options", () => {
396
+ beforeEach(async () => {
397
+ await createTestFile(codebaseDir, "users.ts", "export class UserService {}");
398
+ await createTestFile(codebaseDir, "auth.ts", "export class AuthService {}");
399
+ await createTestFile(codebaseDir, "utils.js", "export function helper() {}");
400
+ await createTestFile(codebaseDir, "data.py", "class DataProcessor: pass");
401
+
402
+ await indexer.indexCodebase(codebaseDir);
403
+ });
404
+
405
+ it("should filter results by file extension", async () => {
406
+ const results = await indexer.searchCode(codebaseDir, "class", {
407
+ fileTypes: [".ts"],
408
+ });
409
+
410
+ results.forEach((result) => {
411
+ expect(result.fileExtension).toBe(".ts");
412
+ });
413
+ });
414
+
415
+ it("should respect search limit", async () => {
416
+ const results = await indexer.searchCode(codebaseDir, "export", {
417
+ limit: 2,
418
+ });
419
+
420
+ expect(results.length).toBeLessThanOrEqual(2);
421
+ });
422
+
423
+ it("should apply score threshold", async () => {
424
+ const results = await indexer.searchCode(codebaseDir, "service", {
425
+ scoreThreshold: 0.8,
426
+ });
427
+
428
+ results.forEach((result) => {
429
+ expect(result.score).toBeGreaterThanOrEqual(0.8);
430
+ });
431
+ });
432
+
433
+ it("should support path pattern filtering", async () => {
434
+ await createTestFile(codebaseDir, "src/api/endpoints.ts", "export const API = {}");
435
+ await indexer.indexCodebase(codebaseDir, { forceReindex: true });
436
+
437
+ const results = await indexer.searchCode(codebaseDir, "export", {
438
+ pathPattern: "src/api/**",
439
+ });
440
+
441
+ expect(Array.isArray(results)).toBe(true);
442
+ });
443
+ });
444
+
445
+ describe("Hybrid search workflow", () => {
446
+ it("should enable and use hybrid search", async () => {
447
+ const hybridConfig = { ...config, enableHybridSearch: true };
448
+ const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
449
+
450
+ await createTestFile(
451
+ codebaseDir,
452
+ "search.ts",
453
+ "function performSearch(query: string) { return results; }"
454
+ );
455
+
456
+ await hybridIndexer.indexCodebase(codebaseDir);
457
+
458
+ const results = await hybridIndexer.searchCode(codebaseDir, "search query");
459
+
460
+ expect(results.length).toBeGreaterThan(0);
461
+ });
462
+
463
+ it("should fallback to standard search if hybrid not available", async () => {
464
+ const hybridConfig = { ...config, enableHybridSearch: true };
465
+ const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
466
+
467
+ // Index without hybrid
468
+ await createTestFile(
469
+ codebaseDir,
470
+ "test.ts",
471
+ `export const testValue = true;
472
+ console.log('Test value configured successfully');
473
+ function validate(): boolean {
474
+ console.log('Validating test value');
475
+ return testValue === true;
476
+ }`
477
+ );
478
+ await indexer.indexCodebase(codebaseDir);
479
+
480
+ // Search with hybrid-enabled indexer but collection without hybrid
481
+ const results = await hybridIndexer.searchCode(codebaseDir, "test");
482
+
483
+ expect(results.length).toBeGreaterThan(0);
484
+ });
485
+ });
486
+
487
+ describe("Large project workflow", () => {
488
+ it("should handle projects with many files", async () => {
489
+ // Create a large project structure
490
+ for (let i = 0; i < 20; i++) {
491
+ await createTestFile(
492
+ codebaseDir,
493
+ `module${i}.ts`,
494
+ `export function func${i}() { return ${i}; }`
495
+ );
496
+ }
497
+
498
+ const stats = await indexer.indexCodebase(codebaseDir);
499
+
500
+ expect(stats.filesScanned).toBe(20);
501
+ expect(stats.filesIndexed).toBe(20);
502
+ expect(stats.status).toBe("completed");
503
+ });
504
+
505
+ it("should handle large files with many chunks", async () => {
506
+ const largeFile = Array(100)
507
+ .fill(null)
508
+ .map((_, i) => `function test${i}() { return ${i}; }`)
509
+ .join("\n\n");
510
+
511
+ await createTestFile(codebaseDir, "large.ts", largeFile);
512
+
513
+ const stats = await indexer.indexCodebase(codebaseDir);
514
+
515
+ expect(stats.chunksCreated).toBeGreaterThan(1);
516
+ });
517
+ });
518
+
519
+ describe("Error handling and recovery", () => {
520
+ it("should continue indexing after encountering errors", async () => {
521
+ await createTestFile(
522
+ codebaseDir,
523
+ "valid.ts",
524
+ "export const validValue = true;\nconsole.log('Valid file');"
525
+ );
526
+ await createTestFile(
527
+ codebaseDir,
528
+ "secrets.ts",
529
+ 'export const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_NOT_REAL_KEY";\nconsole.log("Secrets file");'
530
+ );
531
+
532
+ const stats = await indexer.indexCodebase(codebaseDir);
533
+
534
+ // Should index valid file and report error for secrets file
535
+ expect(stats.filesIndexed).toBe(1);
536
+ expect(stats.errors?.length).toBeGreaterThan(0);
537
+ expect(stats.status).toBe("completed");
538
+ });
539
+
540
+ it("should allow re-indexing after partial failure", async () => {
541
+ await createTestFile(
542
+ codebaseDir,
543
+ "test.ts",
544
+ "export const testData = true;\nconsole.log('Test data loaded');"
545
+ );
546
+
547
+ const stats1 = await indexer.indexCodebase(codebaseDir);
548
+ expect(stats1.status).toBe("completed");
549
+
550
+ // Force re-index
551
+ const stats2 = await indexer.indexCodebase(codebaseDir, { forceReindex: true });
552
+ expect(stats2.status).toBe("completed");
553
+ });
554
+ });
555
+
556
+ describe("Clear and re-index workflow", () => {
557
+ it("should clear index and allow re-indexing", async () => {
558
+ await createTestFile(codebaseDir, "test.ts", "const test = 1;");
559
+
560
+ await indexer.indexCodebase(codebaseDir);
561
+
562
+ let status = await indexer.getIndexStatus(codebaseDir);
563
+ expect(status.isIndexed).toBe(true);
564
+
565
+ await indexer.clearIndex(codebaseDir);
566
+
567
+ status = await indexer.getIndexStatus(codebaseDir);
568
+ expect(status.isIndexed).toBe(false);
569
+
570
+ // Re-index
571
+ const stats = await indexer.indexCodebase(codebaseDir);
572
+ expect(stats.status).toBe("completed");
573
+
574
+ status = await indexer.getIndexStatus(codebaseDir);
575
+ expect(status.isIndexed).toBe(true);
576
+ });
577
+ });
578
+
579
+ describe("Progress tracking", () => {
580
+ it("should report progress through all phases", async () => {
581
+ for (let i = 0; i < 5; i++) {
582
+ await createTestFile(
583
+ codebaseDir,
584
+ `file${i}.ts`,
585
+ `export const value${i} = ${i};\nconsole.log('File ${i} loaded successfully');\nfunction process${i}() { return value${i} * 2; }`
586
+ );
587
+ }
588
+
589
+ const progressUpdates: string[] = [];
590
+ const progressCallback = (progress: any) => {
591
+ progressUpdates.push(progress.phase);
592
+ };
593
+
594
+ await indexer.indexCodebase(codebaseDir, undefined, progressCallback);
595
+
596
+ expect(progressUpdates).toContain("scanning");
597
+ expect(progressUpdates).toContain("chunking");
598
+ expect(progressUpdates).toContain("embedding");
599
+ expect(progressUpdates).toContain("storing");
600
+ });
601
+
602
+ it("should report progress during incremental updates", async () => {
603
+ await createTestFile(
604
+ codebaseDir,
605
+ "file1.ts",
606
+ "export const initial = 1;\nconsole.log('Initial file');"
607
+ );
608
+ await indexer.indexCodebase(codebaseDir);
609
+
610
+ await createTestFile(
611
+ codebaseDir,
612
+ "file2.ts",
613
+ "export const additional = 2;\nconsole.log('Additional file');"
614
+ );
615
+
616
+ const progressUpdates: string[] = [];
617
+ const progressCallback = (progress: any) => {
618
+ progressUpdates.push(progress.phase);
619
+ };
620
+
621
+ await indexer.reindexChanges(codebaseDir, progressCallback);
622
+
623
+ expect(progressUpdates.length).toBeGreaterThan(0);
624
+ });
625
+ });
626
+
627
+ describe("Hybrid search with incremental updates", () => {
628
+ it("should use hybrid search during reindexChanges", async () => {
629
+ const hybridConfig = { ...config, enableHybridSearch: true };
630
+ const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
631
+
632
+ // Initial indexing with hybrid search
633
+ await createTestFile(
634
+ codebaseDir,
635
+ "initial.ts",
636
+ "export const initial = 1;\nconsole.log('Initial file');"
637
+ );
638
+ await hybridIndexer.indexCodebase(codebaseDir);
639
+
640
+ // Add a new file with enough content to create chunks
641
+ await createTestFile(
642
+ codebaseDir,
643
+ "added.ts",
644
+ `export const added = 2;
645
+ console.log('Added file with more content');
646
+
647
+ /**
648
+ * Function to demonstrate hybrid search indexing
649
+ */
650
+ export function processData(data: string[]): string[] {
651
+ console.log('Processing data in hybrid search mode');
652
+ const result = data.map(item => item.toUpperCase());
653
+ return result;
654
+ }
655
+
656
+ export class DataProcessor {
657
+ process(input: string): string {
658
+ return input.trim();
659
+ }
660
+ }`
661
+ );
662
+
663
+ // Reindex with hybrid search - this should cover lines 540-545
664
+ const updateStats = await hybridIndexer.reindexChanges(codebaseDir);
665
+
666
+ expect(updateStats.filesAdded).toBe(1);
667
+ expect(updateStats.chunksAdded).toBeGreaterThan(0);
668
+ });
669
+ });
670
+
671
+ describe("Error handling during reindexChanges", () => {
672
+ it("should handle file processing errors gracefully", async () => {
673
+ // Initial indexing
674
+ await createTestFile(
675
+ codebaseDir,
676
+ "file1.ts",
677
+ "export const value = 1;\nconsole.log('File 1');"
678
+ );
679
+ await indexer.indexCodebase(codebaseDir);
680
+
681
+ // Add a new file
682
+ await createTestFile(
683
+ codebaseDir,
684
+ "file2.ts",
685
+ "export const value2 = 2;\nconsole.log('File 2');"
686
+ );
687
+
688
+ // This should not throw even if there are processing errors
689
+ const stats = await indexer.reindexChanges(codebaseDir);
690
+
691
+ // Stats should still be returned
692
+ expect(stats).toBeDefined();
693
+ expect(stats.filesAdded).toBeGreaterThanOrEqual(0);
694
+ });
695
+ });
696
+ });
697
+
698
+ // Helper function
699
+ async function createTestFile(
700
+ baseDir: string,
701
+ relativePath: string,
702
+ content: string
703
+ ): Promise<void> {
704
+ const fullPath = join(baseDir, relativePath);
705
+ const dir = join(fullPath, "..");
706
+ await fs.mkdir(dir, { recursive: true });
707
+ await fs.writeFile(fullPath, content, "utf-8");
708
+ }