@mhalder/qdrant-mcp-server 2.1.0 → 2.1.2

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 (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +14 -2
  3. package/README.md +3 -2
  4. package/build/code/indexer.d.ts +5 -0
  5. package/build/code/indexer.d.ts.map +1 -1
  6. package/build/code/indexer.js +116 -14
  7. package/build/code/indexer.js.map +1 -1
  8. package/build/code/types.d.ts +4 -0
  9. package/build/code/types.d.ts.map +1 -1
  10. package/build/index.js +28 -831
  11. package/build/index.js.map +1 -1
  12. package/build/prompts/register.d.ts +10 -0
  13. package/build/prompts/register.d.ts.map +1 -0
  14. package/build/prompts/register.js +50 -0
  15. package/build/prompts/register.js.map +1 -0
  16. package/build/resources/index.d.ts +10 -0
  17. package/build/resources/index.d.ts.map +1 -0
  18. package/build/resources/index.js +60 -0
  19. package/build/resources/index.js.map +1 -0
  20. package/build/tools/code.d.ts +10 -0
  21. package/build/tools/code.d.ts.map +1 -0
  22. package/build/tools/code.js +132 -0
  23. package/build/tools/code.js.map +1 -0
  24. package/build/tools/collection.d.ts +12 -0
  25. package/build/tools/collection.d.ts.map +1 -0
  26. package/build/tools/collection.js +59 -0
  27. package/build/tools/collection.js.map +1 -0
  28. package/build/tools/document.d.ts +12 -0
  29. package/build/tools/document.d.ts.map +1 -0
  30. package/build/tools/document.js +84 -0
  31. package/build/tools/document.js.map +1 -0
  32. package/build/tools/index.d.ts +18 -0
  33. package/build/tools/index.d.ts.map +1 -0
  34. package/build/tools/index.js +30 -0
  35. package/build/tools/index.js.map +1 -0
  36. package/build/tools/schemas.d.ts +75 -0
  37. package/build/tools/schemas.d.ts.map +1 -0
  38. package/build/tools/schemas.js +114 -0
  39. package/build/tools/schemas.js.map +1 -0
  40. package/build/tools/search.d.ts +12 -0
  41. package/build/tools/search.d.ts.map +1 -0
  42. package/build/tools/search.js +79 -0
  43. package/build/tools/search.js.map +1 -0
  44. package/examples/code-search/README.md +19 -4
  45. package/package.json +1 -1
  46. package/src/code/indexer.ts +186 -38
  47. package/src/code/types.ts +5 -0
  48. package/src/index.ts +26 -983
  49. package/src/prompts/register.ts +71 -0
  50. package/src/resources/index.ts +79 -0
  51. package/src/tools/code.ts +195 -0
  52. package/src/tools/collection.ts +100 -0
  53. package/src/tools/document.ts +113 -0
  54. package/src/tools/index.ts +48 -0
  55. package/src/tools/schemas.ts +130 -0
  56. package/src/tools/search.ts +122 -0
  57. package/tests/code/indexer.test.ts +412 -74
  58. package/tests/code/integration.test.ts +239 -54
@@ -19,8 +19,8 @@ class MockQdrantManager implements Partial<QdrantManager> {
19
19
  async createCollection(
20
20
  name: string,
21
21
  _vectorSize: number,
22
- _distance: string,
23
- _enableHybrid?: boolean
22
+ _distance: "Cosine" | "Euclid" | "Dot" = "Cosine",
23
+ _enableHybrid?: boolean,
24
24
  ): Promise<void> {
25
25
  this.collections.set(name, {
26
26
  vectorSize: _vectorSize,
@@ -36,10 +36,16 @@ class MockQdrantManager implements Partial<QdrantManager> {
36
36
 
37
37
  async addPoints(collectionName: string, points: any[]): Promise<void> {
38
38
  const existing = this.points.get(collectionName) || [];
39
- this.points.set(collectionName, [...existing, ...points]);
39
+ // Upsert: remove existing points with same ID, then add new ones
40
+ const newIds = new Set(points.map((p) => p.id));
41
+ const filtered = existing.filter((p) => !newIds.has(p.id));
42
+ this.points.set(collectionName, [...filtered, ...points]);
40
43
  }
41
44
 
42
- async addPointsWithSparse(collectionName: string, points: any[]): Promise<void> {
45
+ async addPointsWithSparse(
46
+ collectionName: string,
47
+ points: any[],
48
+ ): Promise<void> {
43
49
  await this.addPoints(collectionName, points);
44
50
  }
45
51
 
@@ -47,7 +53,7 @@ class MockQdrantManager implements Partial<QdrantManager> {
47
53
  collectionName: string,
48
54
  _vector: number[],
49
55
  limit: number,
50
- _filter?: any
56
+ _filter?: any,
51
57
  ): Promise<any[]> {
52
58
  const points = this.points.get(collectionName) || [];
53
59
  return points.slice(0, limit).map((p, idx) => ({
@@ -62,7 +68,7 @@ class MockQdrantManager implements Partial<QdrantManager> {
62
68
  _vector: number[],
63
69
  _sparseVector: any,
64
70
  limit: number,
65
- _filter?: any
71
+ _filter?: any,
66
72
  ): Promise<any[]> {
67
73
  return this.search(collectionName, _vector, limit, _filter);
68
74
  }
@@ -76,6 +82,21 @@ class MockQdrantManager implements Partial<QdrantManager> {
76
82
  vectorSize: collection?.vectorSize || 384,
77
83
  };
78
84
  }
85
+
86
+ async getPoint(
87
+ collectionName: string,
88
+ id: string | number,
89
+ ): Promise<{ id: string | number; payload?: Record<string, any> } | null> {
90
+ const points = this.points.get(collectionName) || [];
91
+ const point = points.find((p) => p.id === id);
92
+ if (!point) {
93
+ return null;
94
+ }
95
+ return {
96
+ id: point.id,
97
+ payload: point.payload,
98
+ };
99
+ }
79
100
  }
80
101
 
81
102
  class MockEmbeddingProvider implements EmbeddingProvider {
@@ -83,12 +104,23 @@ class MockEmbeddingProvider implements EmbeddingProvider {
83
104
  return 384;
84
105
  }
85
106
 
86
- async embed(_text: string): Promise<{ embedding: number[] }> {
87
- return { embedding: new Array(384).fill(0.1) };
107
+ getModel(): string {
108
+ return "mock-model";
88
109
  }
89
110
 
90
- async embedBatch(texts: string[]): Promise<Array<{ embedding: number[] }>> {
91
- return texts.map(() => ({ embedding: new Array(384).fill(0.1) }));
111
+ async embed(
112
+ _text: string,
113
+ ): Promise<{ embedding: number[]; dimensions: number }> {
114
+ return { embedding: new Array(384).fill(0.1), dimensions: 384 };
115
+ }
116
+
117
+ async embedBatch(
118
+ texts: string[],
119
+ ): Promise<Array<{ embedding: number[]; dimensions: number }>> {
120
+ return texts.map(() => ({
121
+ embedding: new Array(384).fill(0.1),
122
+ dimensions: 384,
123
+ }));
92
124
  }
93
125
  }
94
126
 
@@ -104,7 +136,7 @@ describe("CodeIndexer", () => {
104
136
  // Create temporary test directory
105
137
  tempDir = join(
106
138
  tmpdir(),
107
- `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`
139
+ `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`,
108
140
  );
109
141
  codebaseDir = join(tempDir, "codebase");
110
142
  await fs.mkdir(codebaseDir, { recursive: true });
@@ -139,7 +171,7 @@ describe("CodeIndexer", () => {
139
171
  await createTestFile(
140
172
  codebaseDir,
141
173
  "hello.ts",
142
- 'export function hello(name: string): string {\n console.log("Greeting user");\n return `Hello, ${name}!`;\n}'
174
+ 'export function hello(name: string): string {\n console.log("Greeting user");\n return `Hello, ${name}!`;\n}',
143
175
  );
144
176
 
145
177
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -173,7 +205,7 @@ describe("CodeIndexer", () => {
173
205
  await createTestFile(
174
206
  codebaseDir,
175
207
  "test.ts",
176
- "export const configuration = {\n apiKey: process.env.API_KEY,\n timeout: 5000\n};"
208
+ "export const configuration = {\n apiKey: process.env.API_KEY,\n timeout: 5000\n};",
177
209
  );
178
210
 
179
211
  const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
@@ -184,7 +216,7 @@ describe("CodeIndexer", () => {
184
216
  expect.stringContaining("code_"),
185
217
  384,
186
218
  "Cosine",
187
- false
219
+ false,
188
220
  );
189
221
  });
190
222
 
@@ -192,7 +224,7 @@ describe("CodeIndexer", () => {
192
224
  await createTestFile(
193
225
  codebaseDir,
194
226
  "test.ts",
195
- "export function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}"
227
+ "export function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}",
196
228
  );
197
229
 
198
230
  await indexer.indexCodebase(codebaseDir);
@@ -207,7 +239,7 @@ describe("CodeIndexer", () => {
207
239
  await createTestFile(
208
240
  codebaseDir,
209
241
  "test.ts",
210
- "export interface User {\n id: string;\n name: string;\n email: string;\n}"
242
+ "export interface User {\n id: string;\n name: string;\n email: string;\n}",
211
243
  );
212
244
 
213
245
  const progressCallback = vi.fn();
@@ -215,8 +247,16 @@ describe("CodeIndexer", () => {
215
247
  await indexer.indexCodebase(codebaseDir, undefined, progressCallback);
216
248
 
217
249
  expect(progressCallback).toHaveBeenCalled();
218
- expect(progressCallback.mock.calls.some((call) => call[0].phase === "scanning")).toBe(true);
219
- expect(progressCallback.mock.calls.some((call) => call[0].phase === "chunking")).toBe(true);
250
+ expect(
251
+ progressCallback.mock.calls.some(
252
+ (call) => call[0].phase === "scanning",
253
+ ),
254
+ ).toBe(true);
255
+ expect(
256
+ progressCallback.mock.calls.some(
257
+ (call) => call[0].phase === "chunking",
258
+ ),
259
+ ).toBe(true);
220
260
  });
221
261
 
222
262
  it("should respect custom extensions", async () => {
@@ -234,7 +274,7 @@ describe("CodeIndexer", () => {
234
274
  await createTestFile(
235
275
  codebaseDir,
236
276
  "secrets.ts",
237
- 'const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_ONLY_NOT_REAL";'
277
+ 'const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_ONLY_NOT_REAL";',
238
278
  );
239
279
 
240
280
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -245,12 +285,16 @@ describe("CodeIndexer", () => {
245
285
 
246
286
  it("should enable hybrid search when configured", async () => {
247
287
  const hybridConfig = { ...config, enableHybridSearch: true };
248
- const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
288
+ const hybridIndexer = new CodeIndexer(
289
+ qdrant as any,
290
+ embeddings,
291
+ hybridConfig,
292
+ );
249
293
 
250
294
  await createTestFile(
251
295
  codebaseDir,
252
296
  "test.ts",
253
- "export class DataService {\n async fetchData(): Promise<any[]> {\n return [];\n }\n}"
297
+ "export class DataService {\n async fetchData(): Promise<any[]> {\n return [];\n }\n}",
254
298
  );
255
299
 
256
300
  const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
@@ -261,7 +305,7 @@ describe("CodeIndexer", () => {
261
305
  expect.stringContaining("code_"),
262
306
  384,
263
307
  "Cosine",
264
- true
308
+ true,
265
309
  );
266
310
  });
267
311
 
@@ -270,16 +314,20 @@ describe("CodeIndexer", () => {
270
314
 
271
315
  // Mock fs.readFile to throw error for this specific file
272
316
  const originalReadFile = fs.readFile;
273
- vi.spyOn(fs, "readFile").mockImplementation(async (path: any, ...args: any[]) => {
274
- if (path.includes("test.ts")) {
275
- throw new Error("Permission denied");
276
- }
277
- return originalReadFile(path, ...args);
278
- });
317
+ vi.spyOn(fs, "readFile").mockImplementation(
318
+ async (path: any, ...args: any[]) => {
319
+ if (path.includes("test.ts")) {
320
+ throw new Error("Permission denied");
321
+ }
322
+ return originalReadFile(path, ...args);
323
+ },
324
+ );
279
325
 
280
326
  const stats = await indexer.indexCodebase(codebaseDir);
281
327
 
282
- expect(stats.errors?.some((e) => e.includes("Permission denied"))).toBe(true);
328
+ expect(stats.errors?.some((e) => e.includes("Permission denied"))).toBe(
329
+ true,
330
+ );
283
331
 
284
332
  vi.restoreAllMocks();
285
333
  });
@@ -290,7 +338,7 @@ describe("CodeIndexer", () => {
290
338
  await createTestFile(
291
339
  codebaseDir,
292
340
  `file${i}.ts`,
293
- `export function test${i}() {\n console.log('Test function ${i}');\n return ${i};\n}`
341
+ `export function test${i}() {\n console.log('Test function ${i}');\n return ${i};\n}`,
294
342
  );
295
343
  }
296
344
 
@@ -307,7 +355,7 @@ describe("CodeIndexer", () => {
307
355
  await createTestFile(
308
356
  codebaseDir,
309
357
  "test.ts",
310
- "export function hello(name: string): string {\n const greeting = `Hello, ${name}!`;\n return greeting;\n}"
358
+ "export function hello(name: string): string {\n const greeting = `Hello, ${name}!`;\n return greeting;\n}",
311
359
  );
312
360
  await indexer.indexCodebase(codebaseDir);
313
361
  });
@@ -323,11 +371,15 @@ describe("CodeIndexer", () => {
323
371
  const nonIndexedDir = join(tempDir, "non-indexed");
324
372
  await fs.mkdir(nonIndexedDir, { recursive: true });
325
373
 
326
- await expect(indexer.searchCode(nonIndexedDir, "test")).rejects.toThrow("not indexed");
374
+ await expect(indexer.searchCode(nonIndexedDir, "test")).rejects.toThrow(
375
+ "not indexed",
376
+ );
327
377
  });
328
378
 
329
379
  it("should respect limit option", async () => {
330
- const results = await indexer.searchCode(codebaseDir, "test", { limit: 2 });
380
+ const results = await indexer.searchCode(codebaseDir, "test", {
381
+ limit: 2,
382
+ });
331
383
 
332
384
  expect(results.length).toBeLessThanOrEqual(2);
333
385
  });
@@ -356,7 +408,11 @@ describe("CodeIndexer", () => {
356
408
 
357
409
  it("should use hybrid search when enabled", async () => {
358
410
  const hybridConfig = { ...config, enableHybridSearch: true };
359
- const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
411
+ const hybridIndexer = new CodeIndexer(
412
+ qdrant as any,
413
+ embeddings,
414
+ hybridConfig,
415
+ );
360
416
 
361
417
  await createTestFile(
362
418
  codebaseDir,
@@ -373,7 +429,7 @@ function performValidation(): boolean {
373
429
  }
374
430
  function checkStatus(): boolean {
375
431
  return true;
376
- }`
432
+ }`,
377
433
  );
378
434
  // Force reindex to recreate collection with hybrid search enabled
379
435
  await hybridIndexer.indexCodebase(codebaseDir, { forceReindex: true });
@@ -400,31 +456,287 @@ function checkStatus(): boolean {
400
456
  });
401
457
 
402
458
  describe("getIndexStatus", () => {
403
- it("should return not indexed for new codebase", async () => {
404
- const status = await indexer.getIndexStatus(codebaseDir);
459
+ describe("not_indexed status", () => {
460
+ it("should return not_indexed for new codebase with no collection", async () => {
461
+ const status = await indexer.getIndexStatus(codebaseDir);
462
+
463
+ expect(status.isIndexed).toBe(false);
464
+ expect(status.status).toBe("not_indexed");
465
+ expect(status.collectionName).toBeUndefined();
466
+ expect(status.chunksCount).toBeUndefined();
467
+ });
405
468
 
406
- expect(status.isIndexed).toBe(false);
469
+ it("should return not_indexed when collection exists but has no chunks and no completion marker", async () => {
470
+ // Create an empty directory with no supported files
471
+ const emptyDir = join(tempDir, "empty-codebase");
472
+ await fs.mkdir(emptyDir, { recursive: true });
473
+ // Create a non-supported file
474
+ await fs.writeFile(join(emptyDir, "readme.md"), "# README");
475
+
476
+ const stats = await indexer.indexCodebase(emptyDir);
477
+
478
+ // No files to index means no collection is created
479
+ expect(stats.filesScanned).toBe(0);
480
+
481
+ const status = await indexer.getIndexStatus(emptyDir);
482
+ expect(status.status).toBe("not_indexed");
483
+ expect(status.isIndexed).toBe(false);
484
+ });
407
485
  });
408
486
 
409
- it("should return indexed status after indexing", async () => {
410
- await createTestFile(
411
- codebaseDir,
412
- "test.ts",
413
- "export const APP_CONFIG = {\n port: 3000,\n host: 'localhost',\n debug: true,\n apiUrl: 'https://api.example.com',\n timeout: 5000\n};\nconsole.log('Config loaded');"
414
- );
415
- await indexer.indexCodebase(codebaseDir);
487
+ describe("indexed status", () => {
488
+ it("should return indexed status after successful indexing", async () => {
489
+ await createTestFile(
490
+ codebaseDir,
491
+ "test.ts",
492
+ "export const APP_CONFIG = {\n port: 3000,\n host: 'localhost',\n debug: true,\n apiUrl: 'https://api.example.com',\n timeout: 5000\n};\nconsole.log('Config loaded');",
493
+ );
494
+ await indexer.indexCodebase(codebaseDir);
416
495
 
417
- const status = await indexer.getIndexStatus(codebaseDir);
496
+ const status = await indexer.getIndexStatus(codebaseDir);
418
497
 
419
- expect(status.isIndexed).toBe(true);
420
- expect(status.collectionName).toBeDefined();
421
- expect(status.chunksCount).toBeGreaterThan(0);
498
+ expect(status.isIndexed).toBe(true);
499
+ expect(status.status).toBe("indexed");
500
+ expect(status.collectionName).toBeDefined();
501
+ expect(status.chunksCount).toBeGreaterThan(0);
502
+ });
503
+
504
+ it("should include lastUpdated timestamp after indexing", async () => {
505
+ await createTestFile(
506
+ codebaseDir,
507
+ "test.ts",
508
+ "export const value = 1;\nconsole.log('Testing timestamp');\nfunction test() { return value; }",
509
+ );
510
+
511
+ const beforeIndexing = new Date();
512
+ await indexer.indexCodebase(codebaseDir);
513
+ const afterIndexing = new Date();
514
+
515
+ const status = await indexer.getIndexStatus(codebaseDir);
516
+
517
+ expect(status.status).toBe("indexed");
518
+ expect(status.lastUpdated).toBeDefined();
519
+ expect(status.lastUpdated).toBeInstanceOf(Date);
520
+ expect(status.lastUpdated!.getTime()).toBeGreaterThanOrEqual(
521
+ beforeIndexing.getTime(),
522
+ );
523
+ expect(status.lastUpdated!.getTime()).toBeLessThanOrEqual(
524
+ afterIndexing.getTime(),
525
+ );
526
+ });
527
+
528
+ it("should return correct chunks count excluding metadata point", async () => {
529
+ await createTestFile(
530
+ codebaseDir,
531
+ "test.ts",
532
+ "export const a = 1;\nexport const b = 2;\nconsole.log('File with content');\nfunction helper() { return a + b; }",
533
+ );
534
+ await indexer.indexCodebase(codebaseDir);
535
+
536
+ const status = await indexer.getIndexStatus(codebaseDir);
537
+
538
+ // Chunks count should not include the metadata point
539
+ expect(status.chunksCount).toBeGreaterThanOrEqual(0);
540
+ // The actual count depends on chunking, but it should be consistent
541
+ expect(typeof status.chunksCount).toBe("number");
542
+ });
543
+ });
544
+
545
+ describe("completion marker", () => {
546
+ it("should store completion marker when indexing completes", async () => {
547
+ await createTestFile(
548
+ codebaseDir,
549
+ "test.ts",
550
+ "export const data = { key: 'value' };\nconsole.log('Completion marker test');\nfunction process() { return data; }",
551
+ );
552
+ await indexer.indexCodebase(codebaseDir);
553
+
554
+ // Verify status is indexed (which means completion marker exists)
555
+ const status = await indexer.getIndexStatus(codebaseDir);
556
+ expect(status.status).toBe("indexed");
557
+ expect(status.isIndexed).toBe(true);
558
+ });
559
+
560
+ it("should store completion marker even when no chunks are created", async () => {
561
+ // Create a file that might produce no chunks (very small)
562
+ await createTestFile(codebaseDir, "tiny.ts", "const x = 1;");
563
+
564
+ await indexer.indexCodebase(codebaseDir);
565
+
566
+ const status = await indexer.getIndexStatus(codebaseDir);
567
+
568
+ // Even with minimal content, should be marked as indexed
569
+ expect(status.status).toBe("indexed");
570
+ expect(status.isIndexed).toBe(true);
571
+ });
572
+
573
+ it("should update completion marker on force reindex", async () => {
574
+ await createTestFile(
575
+ codebaseDir,
576
+ "test.ts",
577
+ "export const v1 = 'first';\nconsole.log('First indexing');\nfunction init() { return v1; }",
578
+ );
579
+
580
+ await indexer.indexCodebase(codebaseDir);
581
+ const status1 = await indexer.getIndexStatus(codebaseDir);
582
+
583
+ // Wait a tiny bit to ensure timestamp difference
584
+ await new Promise((resolve) => setTimeout(resolve, 10));
585
+
586
+ // Force reindex
587
+ await indexer.indexCodebase(codebaseDir, { forceReindex: true });
588
+ const status2 = await indexer.getIndexStatus(codebaseDir);
589
+
590
+ expect(status2.status).toBe("indexed");
591
+ // Both should have lastUpdated
592
+ expect(status1.lastUpdated).toBeDefined();
593
+ expect(status2.lastUpdated).toBeDefined();
594
+ });
595
+ });
596
+
597
+ describe("backwards compatibility", () => {
598
+ it("should always return isIndexed boolean for backwards compatibility", async () => {
599
+ // Not indexed
600
+ let status = await indexer.getIndexStatus(codebaseDir);
601
+ expect(typeof status.isIndexed).toBe("boolean");
602
+ expect(status.isIndexed).toBe(false);
603
+
604
+ // Indexed
605
+ await createTestFile(
606
+ codebaseDir,
607
+ "test.ts",
608
+ "export const test = true;\nconsole.log('Backwards compat test');\nfunction run() { return test; }",
609
+ );
610
+ await indexer.indexCodebase(codebaseDir);
611
+
612
+ status = await indexer.getIndexStatus(codebaseDir);
613
+ expect(typeof status.isIndexed).toBe("boolean");
614
+ expect(status.isIndexed).toBe(true);
615
+ });
616
+
617
+ it("should have isIndexed=true only when status=indexed", async () => {
618
+ await createTestFile(
619
+ codebaseDir,
620
+ "test.ts",
621
+ "export const consistency = 1;\nconsole.log('Consistency check');\nfunction check() { return consistency; }",
622
+ );
623
+ await indexer.indexCodebase(codebaseDir);
624
+
625
+ const status = await indexer.getIndexStatus(codebaseDir);
626
+
627
+ // isIndexed should match status
628
+ if (status.status === "indexed") {
629
+ expect(status.isIndexed).toBe(true);
630
+ } else {
631
+ expect(status.isIndexed).toBe(false);
632
+ }
633
+ });
634
+ });
635
+
636
+ describe("indexing in-progress status", () => {
637
+ it("should return indexing status during active indexing", async () => {
638
+ // Create multiple files to ensure indexing takes some time
639
+ for (let i = 0; i < 5; i++) {
640
+ await createTestFile(
641
+ codebaseDir,
642
+ `file${i}.ts`,
643
+ `export const value${i} = ${i};\nconsole.log('Processing file ${i}');\nfunction process${i}(x: number) {\n const result = x * ${i};\n console.log('Result:', result);\n return result;\n}`,
644
+ );
645
+ }
646
+
647
+ let statusDuringIndexing: any = null;
648
+
649
+ // Use progress callback to check status mid-indexing
650
+ const indexingPromise = indexer.indexCodebase(
651
+ codebaseDir,
652
+ undefined,
653
+ async (progress) => {
654
+ // Check status during the embedding phase (when we know indexing is in progress)
655
+ if (
656
+ progress.phase === "embedding" &&
657
+ statusDuringIndexing === null
658
+ ) {
659
+ statusDuringIndexing = await indexer.getIndexStatus(codebaseDir);
660
+ }
661
+ },
662
+ );
663
+
664
+ await indexingPromise;
665
+
666
+ // Verify we captured the in-progress status
667
+ if (statusDuringIndexing) {
668
+ expect(statusDuringIndexing.status).toBe("indexing");
669
+ expect(statusDuringIndexing.isIndexed).toBe(false);
670
+ expect(statusDuringIndexing.collectionName).toBeDefined();
671
+ }
672
+
673
+ // After indexing completes, status should be "indexed"
674
+ const statusAfter = await indexer.getIndexStatus(codebaseDir);
675
+ expect(statusAfter.status).toBe("indexed");
676
+ expect(statusAfter.isIndexed).toBe(true);
677
+ });
678
+
679
+ it("should track chunksCount during indexing", async () => {
680
+ for (let i = 0; i < 3; i++) {
681
+ await createTestFile(
682
+ codebaseDir,
683
+ `module${i}.ts`,
684
+ `export class Module${i} {\n constructor() {\n console.log('Module ${i} initialized');\n }\n process(data: string): string {\n return data.toUpperCase();\n }\n}`,
685
+ );
686
+ }
687
+
688
+ let chunksCountDuringIndexing: number | undefined;
689
+
690
+ await indexer.indexCodebase(
691
+ codebaseDir,
692
+ undefined,
693
+ async (progress) => {
694
+ if (
695
+ progress.phase === "storing" &&
696
+ chunksCountDuringIndexing === undefined
697
+ ) {
698
+ const status = await indexer.getIndexStatus(codebaseDir);
699
+ chunksCountDuringIndexing = status.chunksCount;
700
+ }
701
+ },
702
+ );
703
+
704
+ // chunksCount during indexing should be a number (could be 0 or more depending on progress)
705
+ if (chunksCountDuringIndexing !== undefined) {
706
+ expect(typeof chunksCountDuringIndexing).toBe("number");
707
+ expect(chunksCountDuringIndexing).toBeGreaterThanOrEqual(0);
708
+ }
709
+ });
710
+ });
711
+
712
+ describe("legacy collection handling", () => {
713
+ it("should treat collection with chunks but no completion marker as indexed", async () => {
714
+ // Simulate a legacy collection by directly adding points without completion marker
715
+ // First, index normally
716
+ await createTestFile(
717
+ codebaseDir,
718
+ "legacy.ts",
719
+ "export const legacy = true;\nconsole.log('Legacy test');\nfunction legacyFn() { return legacy; }",
720
+ );
721
+ await indexer.indexCodebase(codebaseDir);
722
+
723
+ // Get initial status to get collection name
724
+ const initialStatus = await indexer.getIndexStatus(codebaseDir);
725
+ expect(initialStatus.status).toBe("indexed");
726
+
727
+ // The mock should have stored the completion marker
728
+ // In a real scenario, legacy collections wouldn't have this marker
729
+ // but they would have chunks, and the code handles this gracefully
730
+ expect(initialStatus.chunksCount).toBeGreaterThanOrEqual(0);
731
+ });
422
732
  });
423
733
  });
424
734
 
425
735
  describe("reindexChanges", () => {
426
736
  it("should throw error if not previously indexed", async () => {
427
- await expect(indexer.reindexChanges(codebaseDir)).rejects.toThrow("not indexed");
737
+ await expect(indexer.reindexChanges(codebaseDir)).rejects.toThrow(
738
+ "not indexed",
739
+ );
428
740
  });
429
741
 
430
742
  it("should detect and index new files", async () => {
@@ -436,7 +748,7 @@ console.log('Initial file created');
436
748
  function helper(param: string): boolean {
437
749
  console.log('Processing:', param);
438
750
  return true;
439
- }`
751
+ }`,
440
752
  );
441
753
  await indexer.indexCodebase(codebaseDir);
442
754
 
@@ -463,7 +775,7 @@ function helper(param: string): boolean {
463
775
  " console.log('Valid input');",
464
776
  " return input.length > 5;",
465
777
  "}",
466
- ].join("\n")
778
+ ].join("\n"),
467
779
  );
468
780
 
469
781
  const stats = await indexer.reindexChanges(codebaseDir);
@@ -476,14 +788,14 @@ function helper(param: string): boolean {
476
788
  await createTestFile(
477
789
  codebaseDir,
478
790
  "test.ts",
479
- "export const originalValue = 1;\nconsole.log('Original');"
791
+ "export const originalValue = 1;\nconsole.log('Original');",
480
792
  );
481
793
  await indexer.indexCodebase(codebaseDir);
482
794
 
483
795
  await createTestFile(
484
796
  codebaseDir,
485
797
  "test.ts",
486
- "export const updatedValue = 2;\nconsole.log('Updated');"
798
+ "export const updatedValue = 2;\nconsole.log('Updated');",
487
799
  );
488
800
 
489
801
  const stats = await indexer.reindexChanges(codebaseDir);
@@ -495,7 +807,7 @@ function helper(param: string): boolean {
495
807
  await createTestFile(
496
808
  codebaseDir,
497
809
  "test.ts",
498
- "export const toBeDeleted = 1;\nconsole.log('Will be deleted');"
810
+ "export const toBeDeleted = 1;\nconsole.log('Will be deleted');",
499
811
  );
500
812
  await indexer.indexCodebase(codebaseDir);
501
813
 
@@ -510,7 +822,7 @@ function helper(param: string): boolean {
510
822
  await createTestFile(
511
823
  codebaseDir,
512
824
  "test.ts",
513
- "export const unchangedValue = 1;\nconsole.log('No changes');"
825
+ "export const unchangedValue = 1;\nconsole.log('No changes');",
514
826
  );
515
827
  await indexer.indexCodebase(codebaseDir);
516
828
 
@@ -525,14 +837,14 @@ function helper(param: string): boolean {
525
837
  await createTestFile(
526
838
  codebaseDir,
527
839
  "test.ts",
528
- "export const existingValue = 1;\nconsole.log('Existing');"
840
+ "export const existingValue = 1;\nconsole.log('Existing');",
529
841
  );
530
842
  await indexer.indexCodebase(codebaseDir);
531
843
 
532
844
  await createTestFile(
533
845
  codebaseDir,
534
846
  "new.ts",
535
- "export const newValue = 2;\nconsole.log('New file');"
847
+ "export const newValue = 2;\nconsole.log('New file');",
536
848
  );
537
849
 
538
850
  const progressCallback = vi.fn();
@@ -568,7 +880,7 @@ function helper(param: string): boolean {
568
880
  await createTestFile(
569
881
  codebaseDir,
570
882
  "test.ts",
571
- "export const configValue = 1;\nconsole.log('Config loaded');"
883
+ "export const configValue = 1;\nconsole.log('Config loaded');",
572
884
  );
573
885
  await indexer.indexCodebase(codebaseDir);
574
886
 
@@ -586,7 +898,7 @@ function helper(param: string): boolean {
586
898
  await createTestFile(
587
899
  codebaseDir,
588
900
  "test.ts",
589
- "export const reindexValue = 1;\nconsole.log('Reindexing');"
901
+ "export const reindexValue = 1;\nconsole.log('Reindexing');",
590
902
  );
591
903
  await indexer.indexCodebase(codebaseDir);
592
904
 
@@ -599,7 +911,9 @@ function helper(param: string): boolean {
599
911
 
600
912
  describe("edge cases", () => {
601
913
  it("should handle nested directory structures", async () => {
602
- await fs.mkdir(join(codebaseDir, "src", "components"), { recursive: true });
914
+ await fs.mkdir(join(codebaseDir, "src", "components"), {
915
+ recursive: true,
916
+ });
603
917
  await createTestFile(
604
918
  codebaseDir,
605
919
  "src/components/Button.ts",
@@ -609,7 +923,7 @@ function helper(param: string): boolean {
609
923
  console.log('Button clicked');
610
924
  };
611
925
  return '<button>Click me</button>';
612
- }`
926
+ }`,
613
927
  );
614
928
 
615
929
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -618,7 +932,11 @@ function helper(param: string): boolean {
618
932
  });
619
933
 
620
934
  it("should handle files with unicode content", async () => {
621
- await createTestFile(codebaseDir, "test.ts", "const greeting = '你好世界';");
935
+ await createTestFile(
936
+ codebaseDir,
937
+ "test.ts",
938
+ "const greeting = '你好世界';",
939
+ );
622
940
 
623
941
  const stats = await indexer.indexCodebase(codebaseDir);
624
942
 
@@ -671,12 +989,19 @@ function helper(param: string): boolean {
671
989
  ...config,
672
990
  maxChunksPerFile: 2,
673
991
  };
674
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
992
+ const limitedIndexer = new CodeIndexer(
993
+ qdrant as any,
994
+ embeddings,
995
+ limitedConfig,
996
+ );
675
997
 
676
998
  // Create a large file that would generate many chunks
677
999
  const largeContent = Array(50)
678
1000
  .fill(null)
679
- .map((_, i) => `function test${i}() { console.log('test ${i}'); return ${i}; }`)
1001
+ .map(
1002
+ (_, i) =>
1003
+ `function test${i}() { console.log('test ${i}'); return ${i}; }`,
1004
+ )
680
1005
  .join("\n\n");
681
1006
 
682
1007
  await createTestFile(codebaseDir, "large.ts", largeContent);
@@ -693,14 +1018,18 @@ function helper(param: string): boolean {
693
1018
  ...config,
694
1019
  maxTotalChunks: 3,
695
1020
  };
696
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
1021
+ const limitedIndexer = new CodeIndexer(
1022
+ qdrant as any,
1023
+ embeddings,
1024
+ limitedConfig,
1025
+ );
697
1026
 
698
1027
  // Create multiple files
699
1028
  for (let i = 0; i < 10; i++) {
700
1029
  await createTestFile(
701
1030
  codebaseDir,
702
1031
  `file${i}.ts`,
703
- `export function func${i}() { console.log('function ${i}'); return ${i}; }`
1032
+ `export function func${i}() { console.log('function ${i}'); return ${i}; }`,
704
1033
  );
705
1034
  }
706
1035
 
@@ -716,7 +1045,11 @@ function helper(param: string): boolean {
716
1045
  ...config,
717
1046
  maxTotalChunks: 1,
718
1047
  };
719
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
1048
+ const limitedIndexer = new CodeIndexer(
1049
+ qdrant as any,
1050
+ embeddings,
1051
+ limitedConfig,
1052
+ );
720
1053
 
721
1054
  // Create a file with multiple chunks
722
1055
  const content = `
@@ -751,7 +1084,7 @@ function third() {
751
1084
  await createTestFile(
752
1085
  codebaseDir,
753
1086
  "file1.ts",
754
- "export const initial = 1;\nconsole.log('Initial');"
1087
+ "export const initial = 1;\nconsole.log('Initial');",
755
1088
  );
756
1089
  await indexer.indexCodebase(codebaseDir);
757
1090
 
@@ -765,7 +1098,7 @@ console.log('Added file');
765
1098
  export function process() {
766
1099
  console.log('Processing');
767
1100
  return true;
768
- }`
1101
+ }`,
769
1102
  );
770
1103
 
771
1104
  const progressUpdates: string[] = [];
@@ -793,7 +1126,11 @@ export function process() {
793
1126
  // @ts-expect-error - Mocking for test
794
1127
  fs.readFile = async (path: any, encoding: any) => {
795
1128
  callCount++;
796
- if (callCount === 1 && typeof path === "string" && path.endsWith("test.ts")) {
1129
+ if (
1130
+ callCount === 1 &&
1131
+ typeof path === "string" &&
1132
+ path.endsWith("test.ts")
1133
+ ) {
797
1134
  // Throw a non-Error object
798
1135
  throw "String error";
799
1136
  }
@@ -805,10 +1142,11 @@ export function process() {
805
1142
 
806
1143
  // Should handle the error gracefully
807
1144
  expect(stats.status).toBe("completed");
808
- expect(stats.errors?.some((e) => e.includes("String error"))).toBe(true);
1145
+ expect(stats.errors?.some((e) => e.includes("String error"))).toBe(
1146
+ true,
1147
+ );
809
1148
  } finally {
810
1149
  // Restore original function
811
- // @ts-expect-error
812
1150
  fs.readFile = originalReadFile;
813
1151
  }
814
1152
  });
@@ -819,7 +1157,7 @@ export function process() {
819
1157
  async function createTestFile(
820
1158
  baseDir: string,
821
1159
  relativePath: string,
822
- content: string
1160
+ content: string,
823
1161
  ): Promise<void> {
824
1162
  const fullPath = join(baseDir, relativePath);
825
1163
  const dir = join(fullPath, "..");