@mhalder/qdrant-mcp-server 2.1.1 → 2.2.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.
@@ -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,35 @@ 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
+ }
100
+
101
+ async deletePointsByFilter(
102
+ collectionName: string,
103
+ filter: Record<string, any>,
104
+ ): Promise<void> {
105
+ const points = this.points.get(collectionName) || [];
106
+ const pathToDelete = filter?.must?.[0]?.match?.value;
107
+ if (pathToDelete) {
108
+ const filtered = points.filter(
109
+ (p) => p.payload?.relativePath !== pathToDelete,
110
+ );
111
+ this.points.set(collectionName, filtered);
112
+ }
113
+ }
79
114
  }
80
115
 
81
116
  class MockEmbeddingProvider implements EmbeddingProvider {
@@ -83,12 +118,23 @@ class MockEmbeddingProvider implements EmbeddingProvider {
83
118
  return 384;
84
119
  }
85
120
 
86
- async embed(_text: string): Promise<{ embedding: number[] }> {
87
- return { embedding: new Array(384).fill(0.1) };
121
+ getModel(): string {
122
+ return "mock-model";
88
123
  }
89
124
 
90
- async embedBatch(texts: string[]): Promise<Array<{ embedding: number[] }>> {
91
- return texts.map(() => ({ embedding: new Array(384).fill(0.1) }));
125
+ async embed(
126
+ _text: string,
127
+ ): Promise<{ embedding: number[]; dimensions: number }> {
128
+ return { embedding: new Array(384).fill(0.1), dimensions: 384 };
129
+ }
130
+
131
+ async embedBatch(
132
+ texts: string[],
133
+ ): Promise<Array<{ embedding: number[]; dimensions: number }>> {
134
+ return texts.map(() => ({
135
+ embedding: new Array(384).fill(0.1),
136
+ dimensions: 384,
137
+ }));
92
138
  }
93
139
  }
94
140
 
@@ -104,7 +150,7 @@ describe("CodeIndexer", () => {
104
150
  // Create temporary test directory
105
151
  tempDir = join(
106
152
  tmpdir(),
107
- `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`
153
+ `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`,
108
154
  );
109
155
  codebaseDir = join(tempDir, "codebase");
110
156
  await fs.mkdir(codebaseDir, { recursive: true });
@@ -139,7 +185,7 @@ describe("CodeIndexer", () => {
139
185
  await createTestFile(
140
186
  codebaseDir,
141
187
  "hello.ts",
142
- 'export function hello(name: string): string {\n console.log("Greeting user");\n return `Hello, ${name}!`;\n}'
188
+ 'export function hello(name: string): string {\n console.log("Greeting user");\n return `Hello, ${name}!`;\n}',
143
189
  );
144
190
 
145
191
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -173,7 +219,7 @@ describe("CodeIndexer", () => {
173
219
  await createTestFile(
174
220
  codebaseDir,
175
221
  "test.ts",
176
- "export const configuration = {\n apiKey: process.env.API_KEY,\n timeout: 5000\n};"
222
+ "export const configuration = {\n apiKey: process.env.API_KEY,\n timeout: 5000\n};",
177
223
  );
178
224
 
179
225
  const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
@@ -184,7 +230,7 @@ describe("CodeIndexer", () => {
184
230
  expect.stringContaining("code_"),
185
231
  384,
186
232
  "Cosine",
187
- false
233
+ false,
188
234
  );
189
235
  });
190
236
 
@@ -192,7 +238,7 @@ describe("CodeIndexer", () => {
192
238
  await createTestFile(
193
239
  codebaseDir,
194
240
  "test.ts",
195
- "export function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}"
241
+ "export function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}",
196
242
  );
197
243
 
198
244
  await indexer.indexCodebase(codebaseDir);
@@ -207,7 +253,7 @@ describe("CodeIndexer", () => {
207
253
  await createTestFile(
208
254
  codebaseDir,
209
255
  "test.ts",
210
- "export interface User {\n id: string;\n name: string;\n email: string;\n}"
256
+ "export interface User {\n id: string;\n name: string;\n email: string;\n}",
211
257
  );
212
258
 
213
259
  const progressCallback = vi.fn();
@@ -215,8 +261,16 @@ describe("CodeIndexer", () => {
215
261
  await indexer.indexCodebase(codebaseDir, undefined, progressCallback);
216
262
 
217
263
  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);
264
+ expect(
265
+ progressCallback.mock.calls.some(
266
+ (call) => call[0].phase === "scanning",
267
+ ),
268
+ ).toBe(true);
269
+ expect(
270
+ progressCallback.mock.calls.some(
271
+ (call) => call[0].phase === "chunking",
272
+ ),
273
+ ).toBe(true);
220
274
  });
221
275
 
222
276
  it("should respect custom extensions", async () => {
@@ -234,7 +288,7 @@ describe("CodeIndexer", () => {
234
288
  await createTestFile(
235
289
  codebaseDir,
236
290
  "secrets.ts",
237
- 'const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_ONLY_NOT_REAL";'
291
+ 'const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_ONLY_NOT_REAL";',
238
292
  );
239
293
 
240
294
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -245,12 +299,16 @@ describe("CodeIndexer", () => {
245
299
 
246
300
  it("should enable hybrid search when configured", async () => {
247
301
  const hybridConfig = { ...config, enableHybridSearch: true };
248
- const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
302
+ const hybridIndexer = new CodeIndexer(
303
+ qdrant as any,
304
+ embeddings,
305
+ hybridConfig,
306
+ );
249
307
 
250
308
  await createTestFile(
251
309
  codebaseDir,
252
310
  "test.ts",
253
- "export class DataService {\n async fetchData(): Promise<any[]> {\n return [];\n }\n}"
311
+ "export class DataService {\n async fetchData(): Promise<any[]> {\n return [];\n }\n}",
254
312
  );
255
313
 
256
314
  const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
@@ -261,7 +319,7 @@ describe("CodeIndexer", () => {
261
319
  expect.stringContaining("code_"),
262
320
  384,
263
321
  "Cosine",
264
- true
322
+ true,
265
323
  );
266
324
  });
267
325
 
@@ -270,16 +328,20 @@ describe("CodeIndexer", () => {
270
328
 
271
329
  // Mock fs.readFile to throw error for this specific file
272
330
  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
- });
331
+ vi.spyOn(fs, "readFile").mockImplementation(
332
+ async (path: any, ...args: any[]) => {
333
+ if (path.includes("test.ts")) {
334
+ throw new Error("Permission denied");
335
+ }
336
+ return originalReadFile(path, ...args);
337
+ },
338
+ );
279
339
 
280
340
  const stats = await indexer.indexCodebase(codebaseDir);
281
341
 
282
- expect(stats.errors?.some((e) => e.includes("Permission denied"))).toBe(true);
342
+ expect(stats.errors?.some((e) => e.includes("Permission denied"))).toBe(
343
+ true,
344
+ );
283
345
 
284
346
  vi.restoreAllMocks();
285
347
  });
@@ -290,7 +352,7 @@ describe("CodeIndexer", () => {
290
352
  await createTestFile(
291
353
  codebaseDir,
292
354
  `file${i}.ts`,
293
- `export function test${i}() {\n console.log('Test function ${i}');\n return ${i};\n}`
355
+ `export function test${i}() {\n console.log('Test function ${i}');\n return ${i};\n}`,
294
356
  );
295
357
  }
296
358
 
@@ -307,7 +369,7 @@ describe("CodeIndexer", () => {
307
369
  await createTestFile(
308
370
  codebaseDir,
309
371
  "test.ts",
310
- "export function hello(name: string): string {\n const greeting = `Hello, ${name}!`;\n return greeting;\n}"
372
+ "export function hello(name: string): string {\n const greeting = `Hello, ${name}!`;\n return greeting;\n}",
311
373
  );
312
374
  await indexer.indexCodebase(codebaseDir);
313
375
  });
@@ -323,11 +385,15 @@ describe("CodeIndexer", () => {
323
385
  const nonIndexedDir = join(tempDir, "non-indexed");
324
386
  await fs.mkdir(nonIndexedDir, { recursive: true });
325
387
 
326
- await expect(indexer.searchCode(nonIndexedDir, "test")).rejects.toThrow("not indexed");
388
+ await expect(indexer.searchCode(nonIndexedDir, "test")).rejects.toThrow(
389
+ "not indexed",
390
+ );
327
391
  });
328
392
 
329
393
  it("should respect limit option", async () => {
330
- const results = await indexer.searchCode(codebaseDir, "test", { limit: 2 });
394
+ const results = await indexer.searchCode(codebaseDir, "test", {
395
+ limit: 2,
396
+ });
331
397
 
332
398
  expect(results.length).toBeLessThanOrEqual(2);
333
399
  });
@@ -356,7 +422,11 @@ describe("CodeIndexer", () => {
356
422
 
357
423
  it("should use hybrid search when enabled", async () => {
358
424
  const hybridConfig = { ...config, enableHybridSearch: true };
359
- const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
425
+ const hybridIndexer = new CodeIndexer(
426
+ qdrant as any,
427
+ embeddings,
428
+ hybridConfig,
429
+ );
360
430
 
361
431
  await createTestFile(
362
432
  codebaseDir,
@@ -373,7 +443,7 @@ function performValidation(): boolean {
373
443
  }
374
444
  function checkStatus(): boolean {
375
445
  return true;
376
- }`
446
+ }`,
377
447
  );
378
448
  // Force reindex to recreate collection with hybrid search enabled
379
449
  await hybridIndexer.indexCodebase(codebaseDir, { forceReindex: true });
@@ -400,31 +470,287 @@ function checkStatus(): boolean {
400
470
  });
401
471
 
402
472
  describe("getIndexStatus", () => {
403
- it("should return not indexed for new codebase", async () => {
404
- const status = await indexer.getIndexStatus(codebaseDir);
473
+ describe("not_indexed status", () => {
474
+ it("should return not_indexed for new codebase with no collection", async () => {
475
+ const status = await indexer.getIndexStatus(codebaseDir);
476
+
477
+ expect(status.isIndexed).toBe(false);
478
+ expect(status.status).toBe("not_indexed");
479
+ expect(status.collectionName).toBeUndefined();
480
+ expect(status.chunksCount).toBeUndefined();
481
+ });
405
482
 
406
- expect(status.isIndexed).toBe(false);
483
+ it("should return not_indexed when collection exists but has no chunks and no completion marker", async () => {
484
+ // Create an empty directory with no supported files
485
+ const emptyDir = join(tempDir, "empty-codebase");
486
+ await fs.mkdir(emptyDir, { recursive: true });
487
+ // Create a non-supported file
488
+ await fs.writeFile(join(emptyDir, "readme.md"), "# README");
489
+
490
+ const stats = await indexer.indexCodebase(emptyDir);
491
+
492
+ // No files to index means no collection is created
493
+ expect(stats.filesScanned).toBe(0);
494
+
495
+ const status = await indexer.getIndexStatus(emptyDir);
496
+ expect(status.status).toBe("not_indexed");
497
+ expect(status.isIndexed).toBe(false);
498
+ });
407
499
  });
408
500
 
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);
501
+ describe("indexed status", () => {
502
+ it("should return indexed status after successful indexing", async () => {
503
+ await createTestFile(
504
+ codebaseDir,
505
+ "test.ts",
506
+ "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');",
507
+ );
508
+ await indexer.indexCodebase(codebaseDir);
416
509
 
417
- const status = await indexer.getIndexStatus(codebaseDir);
510
+ const status = await indexer.getIndexStatus(codebaseDir);
511
+
512
+ expect(status.isIndexed).toBe(true);
513
+ expect(status.status).toBe("indexed");
514
+ expect(status.collectionName).toBeDefined();
515
+ expect(status.chunksCount).toBeGreaterThan(0);
516
+ });
517
+
518
+ it("should include lastUpdated timestamp after indexing", async () => {
519
+ await createTestFile(
520
+ codebaseDir,
521
+ "test.ts",
522
+ "export const value = 1;\nconsole.log('Testing timestamp');\nfunction test() { return value; }",
523
+ );
524
+
525
+ const beforeIndexing = new Date();
526
+ await indexer.indexCodebase(codebaseDir);
527
+ const afterIndexing = new Date();
528
+
529
+ const status = await indexer.getIndexStatus(codebaseDir);
530
+
531
+ expect(status.status).toBe("indexed");
532
+ expect(status.lastUpdated).toBeDefined();
533
+ expect(status.lastUpdated).toBeInstanceOf(Date);
534
+ expect(status.lastUpdated!.getTime()).toBeGreaterThanOrEqual(
535
+ beforeIndexing.getTime(),
536
+ );
537
+ expect(status.lastUpdated!.getTime()).toBeLessThanOrEqual(
538
+ afterIndexing.getTime(),
539
+ );
540
+ });
541
+
542
+ it("should return correct chunks count excluding metadata point", async () => {
543
+ await createTestFile(
544
+ codebaseDir,
545
+ "test.ts",
546
+ "export const a = 1;\nexport const b = 2;\nconsole.log('File with content');\nfunction helper() { return a + b; }",
547
+ );
548
+ await indexer.indexCodebase(codebaseDir);
549
+
550
+ const status = await indexer.getIndexStatus(codebaseDir);
551
+
552
+ // Chunks count should not include the metadata point
553
+ expect(status.chunksCount).toBeGreaterThanOrEqual(0);
554
+ // The actual count depends on chunking, but it should be consistent
555
+ expect(typeof status.chunksCount).toBe("number");
556
+ });
557
+ });
558
+
559
+ describe("completion marker", () => {
560
+ it("should store completion marker when indexing completes", async () => {
561
+ await createTestFile(
562
+ codebaseDir,
563
+ "test.ts",
564
+ "export const data = { key: 'value' };\nconsole.log('Completion marker test');\nfunction process() { return data; }",
565
+ );
566
+ await indexer.indexCodebase(codebaseDir);
567
+
568
+ // Verify status is indexed (which means completion marker exists)
569
+ const status = await indexer.getIndexStatus(codebaseDir);
570
+ expect(status.status).toBe("indexed");
571
+ expect(status.isIndexed).toBe(true);
572
+ });
573
+
574
+ it("should store completion marker even when no chunks are created", async () => {
575
+ // Create a file that might produce no chunks (very small)
576
+ await createTestFile(codebaseDir, "tiny.ts", "const x = 1;");
577
+
578
+ await indexer.indexCodebase(codebaseDir);
579
+
580
+ const status = await indexer.getIndexStatus(codebaseDir);
581
+
582
+ // Even with minimal content, should be marked as indexed
583
+ expect(status.status).toBe("indexed");
584
+ expect(status.isIndexed).toBe(true);
585
+ });
586
+
587
+ it("should update completion marker on force reindex", async () => {
588
+ await createTestFile(
589
+ codebaseDir,
590
+ "test.ts",
591
+ "export const v1 = 'first';\nconsole.log('First indexing');\nfunction init() { return v1; }",
592
+ );
593
+
594
+ await indexer.indexCodebase(codebaseDir);
595
+ const status1 = await indexer.getIndexStatus(codebaseDir);
596
+
597
+ // Wait a tiny bit to ensure timestamp difference
598
+ await new Promise((resolve) => setTimeout(resolve, 10));
599
+
600
+ // Force reindex
601
+ await indexer.indexCodebase(codebaseDir, { forceReindex: true });
602
+ const status2 = await indexer.getIndexStatus(codebaseDir);
603
+
604
+ expect(status2.status).toBe("indexed");
605
+ // Both should have lastUpdated
606
+ expect(status1.lastUpdated).toBeDefined();
607
+ expect(status2.lastUpdated).toBeDefined();
608
+ });
609
+ });
610
+
611
+ describe("backwards compatibility", () => {
612
+ it("should always return isIndexed boolean for backwards compatibility", async () => {
613
+ // Not indexed
614
+ let status = await indexer.getIndexStatus(codebaseDir);
615
+ expect(typeof status.isIndexed).toBe("boolean");
616
+ expect(status.isIndexed).toBe(false);
617
+
618
+ // Indexed
619
+ await createTestFile(
620
+ codebaseDir,
621
+ "test.ts",
622
+ "export const test = true;\nconsole.log('Backwards compat test');\nfunction run() { return test; }",
623
+ );
624
+ await indexer.indexCodebase(codebaseDir);
625
+
626
+ status = await indexer.getIndexStatus(codebaseDir);
627
+ expect(typeof status.isIndexed).toBe("boolean");
628
+ expect(status.isIndexed).toBe(true);
629
+ });
630
+
631
+ it("should have isIndexed=true only when status=indexed", async () => {
632
+ await createTestFile(
633
+ codebaseDir,
634
+ "test.ts",
635
+ "export const consistency = 1;\nconsole.log('Consistency check');\nfunction check() { return consistency; }",
636
+ );
637
+ await indexer.indexCodebase(codebaseDir);
638
+
639
+ const status = await indexer.getIndexStatus(codebaseDir);
640
+
641
+ // isIndexed should match status
642
+ if (status.status === "indexed") {
643
+ expect(status.isIndexed).toBe(true);
644
+ } else {
645
+ expect(status.isIndexed).toBe(false);
646
+ }
647
+ });
648
+ });
649
+
650
+ describe("indexing in-progress status", () => {
651
+ it("should return indexing status during active indexing", async () => {
652
+ // Create multiple files to ensure indexing takes some time
653
+ for (let i = 0; i < 5; i++) {
654
+ await createTestFile(
655
+ codebaseDir,
656
+ `file${i}.ts`,
657
+ `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}`,
658
+ );
659
+ }
660
+
661
+ let statusDuringIndexing: any = null;
662
+
663
+ // Use progress callback to check status mid-indexing
664
+ const indexingPromise = indexer.indexCodebase(
665
+ codebaseDir,
666
+ undefined,
667
+ async (progress) => {
668
+ // Check status during the embedding phase (when we know indexing is in progress)
669
+ if (
670
+ progress.phase === "embedding" &&
671
+ statusDuringIndexing === null
672
+ ) {
673
+ statusDuringIndexing = await indexer.getIndexStatus(codebaseDir);
674
+ }
675
+ },
676
+ );
677
+
678
+ await indexingPromise;
679
+
680
+ // Verify we captured the in-progress status
681
+ if (statusDuringIndexing) {
682
+ expect(statusDuringIndexing.status).toBe("indexing");
683
+ expect(statusDuringIndexing.isIndexed).toBe(false);
684
+ expect(statusDuringIndexing.collectionName).toBeDefined();
685
+ }
686
+
687
+ // After indexing completes, status should be "indexed"
688
+ const statusAfter = await indexer.getIndexStatus(codebaseDir);
689
+ expect(statusAfter.status).toBe("indexed");
690
+ expect(statusAfter.isIndexed).toBe(true);
691
+ });
692
+
693
+ it("should track chunksCount during indexing", async () => {
694
+ for (let i = 0; i < 3; i++) {
695
+ await createTestFile(
696
+ codebaseDir,
697
+ `module${i}.ts`,
698
+ `export class Module${i} {\n constructor() {\n console.log('Module ${i} initialized');\n }\n process(data: string): string {\n return data.toUpperCase();\n }\n}`,
699
+ );
700
+ }
701
+
702
+ let chunksCountDuringIndexing: number | undefined;
418
703
 
419
- expect(status.isIndexed).toBe(true);
420
- expect(status.collectionName).toBeDefined();
421
- expect(status.chunksCount).toBeGreaterThan(0);
704
+ await indexer.indexCodebase(
705
+ codebaseDir,
706
+ undefined,
707
+ async (progress) => {
708
+ if (
709
+ progress.phase === "storing" &&
710
+ chunksCountDuringIndexing === undefined
711
+ ) {
712
+ const status = await indexer.getIndexStatus(codebaseDir);
713
+ chunksCountDuringIndexing = status.chunksCount;
714
+ }
715
+ },
716
+ );
717
+
718
+ // chunksCount during indexing should be a number (could be 0 or more depending on progress)
719
+ if (chunksCountDuringIndexing !== undefined) {
720
+ expect(typeof chunksCountDuringIndexing).toBe("number");
721
+ expect(chunksCountDuringIndexing).toBeGreaterThanOrEqual(0);
722
+ }
723
+ });
724
+ });
725
+
726
+ describe("legacy collection handling", () => {
727
+ it("should treat collection with chunks but no completion marker as indexed", async () => {
728
+ // Simulate a legacy collection by directly adding points without completion marker
729
+ // First, index normally
730
+ await createTestFile(
731
+ codebaseDir,
732
+ "legacy.ts",
733
+ "export const legacy = true;\nconsole.log('Legacy test');\nfunction legacyFn() { return legacy; }",
734
+ );
735
+ await indexer.indexCodebase(codebaseDir);
736
+
737
+ // Get initial status to get collection name
738
+ const initialStatus = await indexer.getIndexStatus(codebaseDir);
739
+ expect(initialStatus.status).toBe("indexed");
740
+
741
+ // The mock should have stored the completion marker
742
+ // In a real scenario, legacy collections wouldn't have this marker
743
+ // but they would have chunks, and the code handles this gracefully
744
+ expect(initialStatus.chunksCount).toBeGreaterThanOrEqual(0);
745
+ });
422
746
  });
423
747
  });
424
748
 
425
749
  describe("reindexChanges", () => {
426
750
  it("should throw error if not previously indexed", async () => {
427
- await expect(indexer.reindexChanges(codebaseDir)).rejects.toThrow("not indexed");
751
+ await expect(indexer.reindexChanges(codebaseDir)).rejects.toThrow(
752
+ "not indexed",
753
+ );
428
754
  });
429
755
 
430
756
  it("should detect and index new files", async () => {
@@ -436,7 +762,7 @@ console.log('Initial file created');
436
762
  function helper(param: string): boolean {
437
763
  console.log('Processing:', param);
438
764
  return true;
439
- }`
765
+ }`,
440
766
  );
441
767
  await indexer.indexCodebase(codebaseDir);
442
768
 
@@ -463,7 +789,7 @@ function helper(param: string): boolean {
463
789
  " console.log('Valid input');",
464
790
  " return input.length > 5;",
465
791
  "}",
466
- ].join("\n")
792
+ ].join("\n"),
467
793
  );
468
794
 
469
795
  const stats = await indexer.reindexChanges(codebaseDir);
@@ -476,14 +802,14 @@ function helper(param: string): boolean {
476
802
  await createTestFile(
477
803
  codebaseDir,
478
804
  "test.ts",
479
- "export const originalValue = 1;\nconsole.log('Original');"
805
+ "export const originalValue = 1;\nconsole.log('Original');",
480
806
  );
481
807
  await indexer.indexCodebase(codebaseDir);
482
808
 
483
809
  await createTestFile(
484
810
  codebaseDir,
485
811
  "test.ts",
486
- "export const updatedValue = 2;\nconsole.log('Updated');"
812
+ "export const updatedValue = 2;\nconsole.log('Updated');",
487
813
  );
488
814
 
489
815
  const stats = await indexer.reindexChanges(codebaseDir);
@@ -495,7 +821,7 @@ function helper(param: string): boolean {
495
821
  await createTestFile(
496
822
  codebaseDir,
497
823
  "test.ts",
498
- "export const toBeDeleted = 1;\nconsole.log('Will be deleted');"
824
+ "export const toBeDeleted = 1;\nconsole.log('Will be deleted');",
499
825
  );
500
826
  await indexer.indexCodebase(codebaseDir);
501
827
 
@@ -510,7 +836,7 @@ function helper(param: string): boolean {
510
836
  await createTestFile(
511
837
  codebaseDir,
512
838
  "test.ts",
513
- "export const unchangedValue = 1;\nconsole.log('No changes');"
839
+ "export const unchangedValue = 1;\nconsole.log('No changes');",
514
840
  );
515
841
  await indexer.indexCodebase(codebaseDir);
516
842
 
@@ -525,14 +851,14 @@ function helper(param: string): boolean {
525
851
  await createTestFile(
526
852
  codebaseDir,
527
853
  "test.ts",
528
- "export const existingValue = 1;\nconsole.log('Existing');"
854
+ "export const existingValue = 1;\nconsole.log('Existing');",
529
855
  );
530
856
  await indexer.indexCodebase(codebaseDir);
531
857
 
532
858
  await createTestFile(
533
859
  codebaseDir,
534
860
  "new.ts",
535
- "export const newValue = 2;\nconsole.log('New file');"
861
+ "export const newValue = 2;\nconsole.log('New file');",
536
862
  );
537
863
 
538
864
  const progressCallback = vi.fn();
@@ -540,6 +866,113 @@ function helper(param: string): boolean {
540
866
 
541
867
  expect(progressCallback).toHaveBeenCalled();
542
868
  });
869
+
870
+ it("should delete old chunks when file is modified", async () => {
871
+ await createTestFile(
872
+ codebaseDir,
873
+ "test.ts",
874
+ "export const originalValue = 1;\nconsole.log('Original version');",
875
+ );
876
+ await indexer.indexCodebase(codebaseDir);
877
+
878
+ const deletePointsByFilterSpy = vi.spyOn(qdrant, "deletePointsByFilter");
879
+
880
+ await createTestFile(
881
+ codebaseDir,
882
+ "test.ts",
883
+ "export const modifiedValue = 2;\nconsole.log('Modified version');",
884
+ );
885
+
886
+ await indexer.reindexChanges(codebaseDir);
887
+
888
+ expect(deletePointsByFilterSpy).toHaveBeenCalledWith(
889
+ expect.stringContaining("code_"),
890
+ {
891
+ must: [{ key: "relativePath", match: { value: "test.ts" } }],
892
+ },
893
+ );
894
+ });
895
+
896
+ it("should delete all chunks when file is deleted", async () => {
897
+ await createTestFile(
898
+ codebaseDir,
899
+ "test.ts",
900
+ "export const toDelete = 1;\nconsole.log('Will be deleted');",
901
+ );
902
+ await indexer.indexCodebase(codebaseDir);
903
+
904
+ const deletePointsByFilterSpy = vi.spyOn(qdrant, "deletePointsByFilter");
905
+
906
+ await fs.unlink(join(codebaseDir, "test.ts"));
907
+
908
+ await indexer.reindexChanges(codebaseDir);
909
+
910
+ expect(deletePointsByFilterSpy).toHaveBeenCalledWith(
911
+ expect.stringContaining("code_"),
912
+ {
913
+ must: [{ key: "relativePath", match: { value: "test.ts" } }],
914
+ },
915
+ );
916
+ });
917
+
918
+ it("should not affect chunks from unchanged files", async () => {
919
+ await createTestFile(
920
+ codebaseDir,
921
+ "unchanged.ts",
922
+ "export const unchanged = 1;\nconsole.log('Unchanged');",
923
+ );
924
+ await createTestFile(
925
+ codebaseDir,
926
+ "changed.ts",
927
+ "export const original = 2;\nconsole.log('Original');",
928
+ );
929
+ await indexer.indexCodebase(codebaseDir);
930
+
931
+ const deletePointsByFilterSpy = vi.spyOn(qdrant, "deletePointsByFilter");
932
+
933
+ await createTestFile(
934
+ codebaseDir,
935
+ "changed.ts",
936
+ "export const modified = 3;\nconsole.log('Modified');",
937
+ );
938
+
939
+ await indexer.reindexChanges(codebaseDir);
940
+
941
+ // Should only delete chunks for changed.ts, not unchanged.ts
942
+ expect(deletePointsByFilterSpy).toHaveBeenCalledTimes(1);
943
+ expect(deletePointsByFilterSpy).toHaveBeenCalledWith(
944
+ expect.stringContaining("code_"),
945
+ {
946
+ must: [{ key: "relativePath", match: { value: "changed.ts" } }],
947
+ },
948
+ );
949
+ });
950
+
951
+ it("should handle deletion errors gracefully", async () => {
952
+ await createTestFile(
953
+ codebaseDir,
954
+ "test.ts",
955
+ "export const original = 1;\nconsole.log('Original');",
956
+ );
957
+ await indexer.indexCodebase(codebaseDir);
958
+
959
+ // Mock deletePointsByFilter to throw an error
960
+ const deletePointsByFilterSpy = vi
961
+ .spyOn(qdrant, "deletePointsByFilter")
962
+ .mockRejectedValueOnce(new Error("Deletion failed"));
963
+
964
+ await createTestFile(
965
+ codebaseDir,
966
+ "test.ts",
967
+ "export const modified = 2;\nconsole.log('Modified');",
968
+ );
969
+
970
+ // Should not throw, should continue with reindexing
971
+ const stats = await indexer.reindexChanges(codebaseDir);
972
+
973
+ expect(deletePointsByFilterSpy).toHaveBeenCalled();
974
+ expect(stats.filesModified).toBe(1);
975
+ });
543
976
  });
544
977
 
545
978
  describe("path validation", () => {
@@ -568,7 +1001,7 @@ function helper(param: string): boolean {
568
1001
  await createTestFile(
569
1002
  codebaseDir,
570
1003
  "test.ts",
571
- "export const configValue = 1;\nconsole.log('Config loaded');"
1004
+ "export const configValue = 1;\nconsole.log('Config loaded');",
572
1005
  );
573
1006
  await indexer.indexCodebase(codebaseDir);
574
1007
 
@@ -586,7 +1019,7 @@ function helper(param: string): boolean {
586
1019
  await createTestFile(
587
1020
  codebaseDir,
588
1021
  "test.ts",
589
- "export const reindexValue = 1;\nconsole.log('Reindexing');"
1022
+ "export const reindexValue = 1;\nconsole.log('Reindexing');",
590
1023
  );
591
1024
  await indexer.indexCodebase(codebaseDir);
592
1025
 
@@ -599,7 +1032,9 @@ function helper(param: string): boolean {
599
1032
 
600
1033
  describe("edge cases", () => {
601
1034
  it("should handle nested directory structures", async () => {
602
- await fs.mkdir(join(codebaseDir, "src", "components"), { recursive: true });
1035
+ await fs.mkdir(join(codebaseDir, "src", "components"), {
1036
+ recursive: true,
1037
+ });
603
1038
  await createTestFile(
604
1039
  codebaseDir,
605
1040
  "src/components/Button.ts",
@@ -609,7 +1044,7 @@ function helper(param: string): boolean {
609
1044
  console.log('Button clicked');
610
1045
  };
611
1046
  return '<button>Click me</button>';
612
- }`
1047
+ }`,
613
1048
  );
614
1049
 
615
1050
  const stats = await indexer.indexCodebase(codebaseDir);
@@ -618,7 +1053,11 @@ function helper(param: string): boolean {
618
1053
  });
619
1054
 
620
1055
  it("should handle files with unicode content", async () => {
621
- await createTestFile(codebaseDir, "test.ts", "const greeting = '你好世界';");
1056
+ await createTestFile(
1057
+ codebaseDir,
1058
+ "test.ts",
1059
+ "const greeting = '你好世界';",
1060
+ );
622
1061
 
623
1062
  const stats = await indexer.indexCodebase(codebaseDir);
624
1063
 
@@ -671,12 +1110,19 @@ function helper(param: string): boolean {
671
1110
  ...config,
672
1111
  maxChunksPerFile: 2,
673
1112
  };
674
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
1113
+ const limitedIndexer = new CodeIndexer(
1114
+ qdrant as any,
1115
+ embeddings,
1116
+ limitedConfig,
1117
+ );
675
1118
 
676
1119
  // Create a large file that would generate many chunks
677
1120
  const largeContent = Array(50)
678
1121
  .fill(null)
679
- .map((_, i) => `function test${i}() { console.log('test ${i}'); return ${i}; }`)
1122
+ .map(
1123
+ (_, i) =>
1124
+ `function test${i}() { console.log('test ${i}'); return ${i}; }`,
1125
+ )
680
1126
  .join("\n\n");
681
1127
 
682
1128
  await createTestFile(codebaseDir, "large.ts", largeContent);
@@ -693,14 +1139,18 @@ function helper(param: string): boolean {
693
1139
  ...config,
694
1140
  maxTotalChunks: 3,
695
1141
  };
696
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
1142
+ const limitedIndexer = new CodeIndexer(
1143
+ qdrant as any,
1144
+ embeddings,
1145
+ limitedConfig,
1146
+ );
697
1147
 
698
1148
  // Create multiple files
699
1149
  for (let i = 0; i < 10; i++) {
700
1150
  await createTestFile(
701
1151
  codebaseDir,
702
1152
  `file${i}.ts`,
703
- `export function func${i}() { console.log('function ${i}'); return ${i}; }`
1153
+ `export function func${i}() { console.log('function ${i}'); return ${i}; }`,
704
1154
  );
705
1155
  }
706
1156
 
@@ -716,7 +1166,11 @@ function helper(param: string): boolean {
716
1166
  ...config,
717
1167
  maxTotalChunks: 1,
718
1168
  };
719
- const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
1169
+ const limitedIndexer = new CodeIndexer(
1170
+ qdrant as any,
1171
+ embeddings,
1172
+ limitedConfig,
1173
+ );
720
1174
 
721
1175
  // Create a file with multiple chunks
722
1176
  const content = `
@@ -751,7 +1205,7 @@ function third() {
751
1205
  await createTestFile(
752
1206
  codebaseDir,
753
1207
  "file1.ts",
754
- "export const initial = 1;\nconsole.log('Initial');"
1208
+ "export const initial = 1;\nconsole.log('Initial');",
755
1209
  );
756
1210
  await indexer.indexCodebase(codebaseDir);
757
1211
 
@@ -765,7 +1219,7 @@ console.log('Added file');
765
1219
  export function process() {
766
1220
  console.log('Processing');
767
1221
  return true;
768
- }`
1222
+ }`,
769
1223
  );
770
1224
 
771
1225
  const progressUpdates: string[] = [];
@@ -793,7 +1247,11 @@ export function process() {
793
1247
  // @ts-expect-error - Mocking for test
794
1248
  fs.readFile = async (path: any, encoding: any) => {
795
1249
  callCount++;
796
- if (callCount === 1 && typeof path === "string" && path.endsWith("test.ts")) {
1250
+ if (
1251
+ callCount === 1 &&
1252
+ typeof path === "string" &&
1253
+ path.endsWith("test.ts")
1254
+ ) {
797
1255
  // Throw a non-Error object
798
1256
  throw "String error";
799
1257
  }
@@ -805,10 +1263,11 @@ export function process() {
805
1263
 
806
1264
  // Should handle the error gracefully
807
1265
  expect(stats.status).toBe("completed");
808
- expect(stats.errors?.some((e) => e.includes("String error"))).toBe(true);
1266
+ expect(stats.errors?.some((e) => e.includes("String error"))).toBe(
1267
+ true,
1268
+ );
809
1269
  } finally {
810
1270
  // Restore original function
811
- // @ts-expect-error
812
1271
  fs.readFile = originalReadFile;
813
1272
  }
814
1273
  });
@@ -819,7 +1278,7 @@ export function process() {
819
1278
  async function createTestFile(
820
1279
  baseDir: string,
821
1280
  relativePath: string,
822
- content: string
1281
+ content: string,
823
1282
  ): Promise<void> {
824
1283
  const fullPath = join(baseDir, relativePath);
825
1284
  const dir = join(fullPath, "..");