@mhalder/qdrant-mcp-server 1.3.1 → 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 (118) hide show
  1. package/.codecov.yml +16 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +304 -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 +321 -6
  49. package/build/index.js.map +1 -1
  50. package/build/prompts/index.d.ts +7 -0
  51. package/build/prompts/index.d.ts.map +1 -0
  52. package/build/prompts/index.js +7 -0
  53. package/build/prompts/index.js.map +1 -0
  54. package/build/prompts/index.test.d.ts +2 -0
  55. package/build/prompts/index.test.d.ts.map +1 -0
  56. package/build/prompts/index.test.js +25 -0
  57. package/build/prompts/index.test.js.map +1 -0
  58. package/build/prompts/loader.d.ts +25 -0
  59. package/build/prompts/loader.d.ts.map +1 -0
  60. package/build/prompts/loader.js +81 -0
  61. package/build/prompts/loader.js.map +1 -0
  62. package/build/prompts/loader.test.d.ts +2 -0
  63. package/build/prompts/loader.test.d.ts.map +1 -0
  64. package/build/prompts/loader.test.js +417 -0
  65. package/build/prompts/loader.test.js.map +1 -0
  66. package/build/prompts/template.d.ts +20 -0
  67. package/build/prompts/template.d.ts.map +1 -0
  68. package/build/prompts/template.js +52 -0
  69. package/build/prompts/template.js.map +1 -0
  70. package/build/prompts/template.test.d.ts +2 -0
  71. package/build/prompts/template.test.d.ts.map +1 -0
  72. package/build/prompts/template.test.js +163 -0
  73. package/build/prompts/template.test.js.map +1 -0
  74. package/build/prompts/types.d.ts +34 -0
  75. package/build/prompts/types.d.ts.map +1 -0
  76. package/build/prompts/types.js +5 -0
  77. package/build/prompts/types.js.map +1 -0
  78. package/examples/code-search/README.md +271 -0
  79. package/package.json +13 -1
  80. package/prompts.example.json +96 -0
  81. package/src/code/chunker/base.ts +22 -0
  82. package/src/code/chunker/character-chunker.ts +131 -0
  83. package/src/code/chunker/tree-sitter-chunker.ts +250 -0
  84. package/src/code/config.ts +156 -0
  85. package/src/code/indexer.ts +613 -0
  86. package/src/code/metadata.ts +153 -0
  87. package/src/code/scanner.ts +124 -0
  88. package/src/code/sync/merkle.ts +136 -0
  89. package/src/code/sync/snapshot.ts +110 -0
  90. package/src/code/sync/synchronizer.ts +154 -0
  91. package/src/code/types.ts +117 -0
  92. package/src/index.ts +382 -5
  93. package/src/prompts/index.test.ts +29 -0
  94. package/src/prompts/index.ts +7 -0
  95. package/src/prompts/loader.test.ts +494 -0
  96. package/src/prompts/loader.ts +90 -0
  97. package/src/prompts/template.test.ts +212 -0
  98. package/src/prompts/template.ts +69 -0
  99. package/src/prompts/types.ts +37 -0
  100. package/tests/code/chunker/character-chunker.test.ts +141 -0
  101. package/tests/code/chunker/tree-sitter-chunker.test.ts +275 -0
  102. package/tests/code/fixtures/sample-py/calculator.py +32 -0
  103. package/tests/code/fixtures/sample-ts/async-operations.ts +120 -0
  104. package/tests/code/fixtures/sample-ts/auth.ts +31 -0
  105. package/tests/code/fixtures/sample-ts/config.ts +52 -0
  106. package/tests/code/fixtures/sample-ts/database.ts +50 -0
  107. package/tests/code/fixtures/sample-ts/index.ts +39 -0
  108. package/tests/code/fixtures/sample-ts/types-advanced.ts +132 -0
  109. package/tests/code/fixtures/sample-ts/utils.ts +105 -0
  110. package/tests/code/fixtures/sample-ts/validator.ts +169 -0
  111. package/tests/code/indexer.test.ts +828 -0
  112. package/tests/code/integration.test.ts +708 -0
  113. package/tests/code/metadata.test.ts +457 -0
  114. package/tests/code/scanner.test.ts +131 -0
  115. package/tests/code/sync/merkle.test.ts +406 -0
  116. package/tests/code/sync/snapshot.test.ts +360 -0
  117. package/tests/code/sync/synchronizer.test.ts +501 -0
  118. package/vitest.config.ts +1 -0
@@ -0,0 +1,828 @@
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, vi } 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
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: _vectorSize,
27
+ hybridEnabled: _enableHybrid || false,
28
+ });
29
+ this.points.set(name, []);
30
+ }
31
+
32
+ async deleteCollection(name: string): Promise<void> {
33
+ this.collections.delete(name);
34
+ this.points.delete(name);
35
+ }
36
+
37
+ async addPoints(collectionName: string, points: any[]): Promise<void> {
38
+ const existing = this.points.get(collectionName) || [];
39
+ this.points.set(collectionName, [...existing, ...points]);
40
+ }
41
+
42
+ async addPointsWithSparse(collectionName: string, points: any[]): Promise<void> {
43
+ await this.addPoints(collectionName, points);
44
+ }
45
+
46
+ async search(
47
+ collectionName: string,
48
+ _vector: number[],
49
+ limit: number,
50
+ _filter?: any
51
+ ): Promise<any[]> {
52
+ const points = this.points.get(collectionName) || [];
53
+ return points.slice(0, limit).map((p, idx) => ({
54
+ id: p.id,
55
+ score: 0.9 - idx * 0.1,
56
+ payload: p.payload,
57
+ }));
58
+ }
59
+
60
+ async hybridSearch(
61
+ collectionName: string,
62
+ _vector: number[],
63
+ _sparseVector: any,
64
+ limit: number,
65
+ _filter?: any
66
+ ): Promise<any[]> {
67
+ return this.search(collectionName, _vector, limit, _filter);
68
+ }
69
+
70
+ async getCollectionInfo(name: string): Promise<any> {
71
+ const collection = this.collections.get(name);
72
+ const points = this.points.get(name) || [];
73
+ return {
74
+ pointsCount: points.length,
75
+ hybridEnabled: collection?.hybridEnabled || false,
76
+ vectorSize: collection?.vectorSize || 384,
77
+ };
78
+ }
79
+ }
80
+
81
+ class MockEmbeddingProvider implements EmbeddingProvider {
82
+ getDimensions(): number {
83
+ return 384;
84
+ }
85
+
86
+ async embed(_text: string): Promise<{ embedding: number[] }> {
87
+ return { embedding: new Array(384).fill(0.1) };
88
+ }
89
+
90
+ async embedBatch(texts: string[]): Promise<Array<{ embedding: number[] }>> {
91
+ return texts.map(() => ({ embedding: new Array(384).fill(0.1) }));
92
+ }
93
+ }
94
+
95
+ describe("CodeIndexer", () => {
96
+ let indexer: CodeIndexer;
97
+ let qdrant: MockQdrantManager;
98
+ let embeddings: MockEmbeddingProvider;
99
+ let config: CodeConfig;
100
+ let tempDir: string;
101
+ let codebaseDir: string;
102
+
103
+ beforeEach(async () => {
104
+ // Create temporary test directory
105
+ tempDir = join(
106
+ tmpdir(),
107
+ `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`
108
+ );
109
+ codebaseDir = join(tempDir, "codebase");
110
+ await fs.mkdir(codebaseDir, { recursive: true });
111
+
112
+ // Initialize mocks and config
113
+ qdrant = new MockQdrantManager() as any;
114
+ embeddings = new MockEmbeddingProvider();
115
+ config = {
116
+ chunkSize: 500,
117
+ chunkOverlap: 50,
118
+ enableASTChunking: true,
119
+ supportedExtensions: [".ts", ".js", ".py"],
120
+ ignorePatterns: ["node_modules/**", "dist/**"],
121
+ batchSize: 10,
122
+ defaultSearchLimit: 5,
123
+ enableHybridSearch: false,
124
+ };
125
+
126
+ indexer = new CodeIndexer(qdrant as any, embeddings, config);
127
+ });
128
+
129
+ afterEach(async () => {
130
+ try {
131
+ await fs.rm(tempDir, { recursive: true, force: true });
132
+ } catch (_error) {
133
+ // Ignore cleanup errors
134
+ }
135
+ });
136
+
137
+ describe("indexCodebase", () => {
138
+ it("should index a simple codebase", async () => {
139
+ await createTestFile(
140
+ codebaseDir,
141
+ "hello.ts",
142
+ 'export function hello(name: string): string {\n console.log("Greeting user");\n return `Hello, ${name}!`;\n}'
143
+ );
144
+
145
+ const stats = await indexer.indexCodebase(codebaseDir);
146
+
147
+ expect(stats.filesScanned).toBe(1);
148
+ expect(stats.filesIndexed).toBe(1);
149
+ expect(stats.chunksCreated).toBeGreaterThan(0);
150
+ expect(stats.status).toBe("completed");
151
+ });
152
+
153
+ it("should handle empty directory", async () => {
154
+ const stats = await indexer.indexCodebase(codebaseDir);
155
+
156
+ expect(stats.filesScanned).toBe(0);
157
+ expect(stats.chunksCreated).toBe(0);
158
+ expect(stats.status).toBe("completed");
159
+ });
160
+
161
+ it("should index multiple files", async () => {
162
+ await createTestFile(codebaseDir, "file1.ts", "function test1() {}");
163
+ await createTestFile(codebaseDir, "file2.js", "function test2() {}");
164
+ await createTestFile(codebaseDir, "file3.py", "def test3(): pass");
165
+
166
+ const stats = await indexer.indexCodebase(codebaseDir);
167
+
168
+ expect(stats.filesScanned).toBe(3);
169
+ expect(stats.filesIndexed).toBe(3);
170
+ });
171
+
172
+ it("should create collection with correct settings", async () => {
173
+ await createTestFile(
174
+ codebaseDir,
175
+ "test.ts",
176
+ "export const configuration = {\n apiKey: process.env.API_KEY,\n timeout: 5000\n};"
177
+ );
178
+
179
+ const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
180
+
181
+ await indexer.indexCodebase(codebaseDir);
182
+
183
+ expect(createCollectionSpy).toHaveBeenCalledWith(
184
+ expect.stringContaining("code_"),
185
+ 384,
186
+ "Cosine",
187
+ false
188
+ );
189
+ });
190
+
191
+ it("should force re-index when option is set", async () => {
192
+ await createTestFile(
193
+ codebaseDir,
194
+ "test.ts",
195
+ "export function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}"
196
+ );
197
+
198
+ await indexer.indexCodebase(codebaseDir);
199
+ const deleteCollectionSpy = vi.spyOn(qdrant, "deleteCollection");
200
+
201
+ await indexer.indexCodebase(codebaseDir, { forceReindex: true });
202
+
203
+ expect(deleteCollectionSpy).toHaveBeenCalled();
204
+ });
205
+
206
+ it("should call progress callback", async () => {
207
+ await createTestFile(
208
+ codebaseDir,
209
+ "test.ts",
210
+ "export interface User {\n id: string;\n name: string;\n email: string;\n}"
211
+ );
212
+
213
+ const progressCallback = vi.fn();
214
+
215
+ await indexer.indexCodebase(codebaseDir, undefined, progressCallback);
216
+
217
+ 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);
220
+ });
221
+
222
+ it("should respect custom extensions", async () => {
223
+ await createTestFile(codebaseDir, "test.ts", "const x = 1;");
224
+ await createTestFile(codebaseDir, "test.md", "# Documentation");
225
+
226
+ const stats = await indexer.indexCodebase(codebaseDir, {
227
+ extensions: [".md"],
228
+ });
229
+
230
+ expect(stats.filesScanned).toBe(1);
231
+ });
232
+
233
+ it("should handle files with secrets gracefully", async () => {
234
+ await createTestFile(
235
+ codebaseDir,
236
+ "secrets.ts",
237
+ 'const apiKey = "sk_test_FAKE_KEY_FOR_TESTING_ONLY_NOT_REAL";'
238
+ );
239
+
240
+ const stats = await indexer.indexCodebase(codebaseDir);
241
+
242
+ expect(stats.errors).toBeDefined();
243
+ expect(stats.errors?.some((e) => e.includes("secrets"))).toBe(true);
244
+ });
245
+
246
+ it("should enable hybrid search when configured", async () => {
247
+ const hybridConfig = { ...config, enableHybridSearch: true };
248
+ const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
249
+
250
+ await createTestFile(
251
+ codebaseDir,
252
+ "test.ts",
253
+ "export class DataService {\n async fetchData(): Promise<any[]> {\n return [];\n }\n}"
254
+ );
255
+
256
+ const createCollectionSpy = vi.spyOn(qdrant, "createCollection");
257
+
258
+ await hybridIndexer.indexCodebase(codebaseDir);
259
+
260
+ expect(createCollectionSpy).toHaveBeenCalledWith(
261
+ expect.stringContaining("code_"),
262
+ 384,
263
+ "Cosine",
264
+ true
265
+ );
266
+ });
267
+
268
+ it("should handle file read errors", async () => {
269
+ await createTestFile(codebaseDir, "test.ts", "content");
270
+
271
+ // Mock fs.readFile to throw error for this specific file
272
+ 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
+ });
279
+
280
+ const stats = await indexer.indexCodebase(codebaseDir);
281
+
282
+ expect(stats.errors?.some((e) => e.includes("Permission denied"))).toBe(true);
283
+
284
+ vi.restoreAllMocks();
285
+ });
286
+
287
+ it("should batch embed operations", async () => {
288
+ // Create multiple files to trigger batching
289
+ for (let i = 0; i < 5; i++) {
290
+ await createTestFile(
291
+ codebaseDir,
292
+ `file${i}.ts`,
293
+ `export function test${i}() {\n console.log('Test function ${i}');\n return ${i};\n}`
294
+ );
295
+ }
296
+
297
+ const embedBatchSpy = vi.spyOn(embeddings, "embedBatch");
298
+
299
+ await indexer.indexCodebase(codebaseDir);
300
+
301
+ expect(embedBatchSpy).toHaveBeenCalled();
302
+ });
303
+ });
304
+
305
+ describe("searchCode", () => {
306
+ beforeEach(async () => {
307
+ await createTestFile(
308
+ codebaseDir,
309
+ "test.ts",
310
+ "export function hello(name: string): string {\n const greeting = `Hello, ${name}!`;\n return greeting;\n}"
311
+ );
312
+ await indexer.indexCodebase(codebaseDir);
313
+ });
314
+
315
+ it("should search indexed codebase", async () => {
316
+ const results = await indexer.searchCode(codebaseDir, "hello function");
317
+
318
+ expect(Array.isArray(results)).toBe(true);
319
+ expect(results.length).toBeGreaterThan(0);
320
+ });
321
+
322
+ it("should throw error for non-indexed codebase", async () => {
323
+ const nonIndexedDir = join(tempDir, "non-indexed");
324
+ await fs.mkdir(nonIndexedDir, { recursive: true });
325
+
326
+ await expect(indexer.searchCode(nonIndexedDir, "test")).rejects.toThrow("not indexed");
327
+ });
328
+
329
+ it("should respect limit option", async () => {
330
+ const results = await indexer.searchCode(codebaseDir, "test", { limit: 2 });
331
+
332
+ expect(results.length).toBeLessThanOrEqual(2);
333
+ });
334
+
335
+ it("should apply score threshold", async () => {
336
+ const results = await indexer.searchCode(codebaseDir, "test", {
337
+ scoreThreshold: 0.95,
338
+ });
339
+
340
+ results.forEach((r) => {
341
+ expect(r.score).toBeGreaterThanOrEqual(0.95);
342
+ });
343
+ });
344
+
345
+ it("should filter by file types", async () => {
346
+ await createTestFile(codebaseDir, "test.py", "def test(): pass");
347
+ await indexer.indexCodebase(codebaseDir, { forceReindex: true });
348
+
349
+ const results = await indexer.searchCode(codebaseDir, "test", {
350
+ fileTypes: [".py"],
351
+ });
352
+
353
+ // Filter should be applied (even if mock doesn't filter properly)
354
+ expect(Array.isArray(results)).toBe(true);
355
+ });
356
+
357
+ it("should use hybrid search when enabled", async () => {
358
+ const hybridConfig = { ...config, enableHybridSearch: true };
359
+ const hybridIndexer = new CodeIndexer(qdrant as any, embeddings, hybridConfig);
360
+
361
+ await createTestFile(
362
+ codebaseDir,
363
+ "test.ts",
364
+ `export function testFunction(): boolean {
365
+ console.log('Running comprehensive test suite');
366
+ const result = performValidation();
367
+ const status = checkStatus();
368
+ return result && status;
369
+ }
370
+ function performValidation(): boolean {
371
+ console.log('Validating data');
372
+ return true;
373
+ }
374
+ function checkStatus(): boolean {
375
+ return true;
376
+ }`
377
+ );
378
+ // Force reindex to recreate collection with hybrid search enabled
379
+ await hybridIndexer.indexCodebase(codebaseDir, { forceReindex: true });
380
+
381
+ const hybridSearchSpy = vi.spyOn(qdrant, "hybridSearch");
382
+
383
+ await hybridIndexer.searchCode(codebaseDir, "test");
384
+
385
+ expect(hybridSearchSpy).toHaveBeenCalled();
386
+ });
387
+
388
+ it("should format results correctly", async () => {
389
+ const results = await indexer.searchCode(codebaseDir, "hello");
390
+
391
+ results.forEach((result) => {
392
+ expect(result).toHaveProperty("content");
393
+ expect(result).toHaveProperty("filePath");
394
+ expect(result).toHaveProperty("startLine");
395
+ expect(result).toHaveProperty("endLine");
396
+ expect(result).toHaveProperty("language");
397
+ expect(result).toHaveProperty("score");
398
+ });
399
+ });
400
+ });
401
+
402
+ describe("getIndexStatus", () => {
403
+ it("should return not indexed for new codebase", async () => {
404
+ const status = await indexer.getIndexStatus(codebaseDir);
405
+
406
+ expect(status.isIndexed).toBe(false);
407
+ });
408
+
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);
416
+
417
+ const status = await indexer.getIndexStatus(codebaseDir);
418
+
419
+ expect(status.isIndexed).toBe(true);
420
+ expect(status.collectionName).toBeDefined();
421
+ expect(status.chunksCount).toBeGreaterThan(0);
422
+ });
423
+ });
424
+
425
+ describe("reindexChanges", () => {
426
+ it("should throw error if not previously indexed", async () => {
427
+ await expect(indexer.reindexChanges(codebaseDir)).rejects.toThrow("not indexed");
428
+ });
429
+
430
+ it("should detect and index new files", async () => {
431
+ await createTestFile(
432
+ codebaseDir,
433
+ "file1.ts",
434
+ `export const initialValue = 1;
435
+ console.log('Initial file created');
436
+ function helper(param: string): boolean {
437
+ console.log('Processing:', param);
438
+ return true;
439
+ }`
440
+ );
441
+ await indexer.indexCodebase(codebaseDir);
442
+
443
+ await createTestFile(
444
+ codebaseDir,
445
+ "file2.ts",
446
+ [
447
+ "export function process(data: number): number {",
448
+ " console.log('Processing data with value:', data);",
449
+ " const multiplier = 42;",
450
+ " const result = data * multiplier;",
451
+ " console.log('Computed result:', result);",
452
+ " if (result > 100) {",
453
+ " console.log('Result is large');",
454
+ " }",
455
+ " return result;",
456
+ "}",
457
+ "",
458
+ "export function validate(input: string): boolean {",
459
+ " if (!input || input.length === 0) {",
460
+ " console.log('Invalid input');",
461
+ " return false;",
462
+ " }",
463
+ " console.log('Valid input');",
464
+ " return input.length > 5;",
465
+ "}",
466
+ ].join("\n")
467
+ );
468
+
469
+ const stats = await indexer.reindexChanges(codebaseDir);
470
+
471
+ expect(stats.filesAdded).toBe(1);
472
+ expect(stats.chunksAdded).toBeGreaterThan(0);
473
+ });
474
+
475
+ it("should detect modified files", async () => {
476
+ await createTestFile(
477
+ codebaseDir,
478
+ "test.ts",
479
+ "export const originalValue = 1;\nconsole.log('Original');"
480
+ );
481
+ await indexer.indexCodebase(codebaseDir);
482
+
483
+ await createTestFile(
484
+ codebaseDir,
485
+ "test.ts",
486
+ "export const updatedValue = 2;\nconsole.log('Updated');"
487
+ );
488
+
489
+ const stats = await indexer.reindexChanges(codebaseDir);
490
+
491
+ expect(stats.filesModified).toBe(1);
492
+ });
493
+
494
+ it("should detect deleted files", async () => {
495
+ await createTestFile(
496
+ codebaseDir,
497
+ "test.ts",
498
+ "export const toBeDeleted = 1;\nconsole.log('Will be deleted');"
499
+ );
500
+ await indexer.indexCodebase(codebaseDir);
501
+
502
+ await fs.unlink(join(codebaseDir, "test.ts"));
503
+
504
+ const stats = await indexer.reindexChanges(codebaseDir);
505
+
506
+ expect(stats.filesDeleted).toBe(1);
507
+ });
508
+
509
+ it("should handle no changes", async () => {
510
+ await createTestFile(
511
+ codebaseDir,
512
+ "test.ts",
513
+ "export const unchangedValue = 1;\nconsole.log('No changes');"
514
+ );
515
+ await indexer.indexCodebase(codebaseDir);
516
+
517
+ const stats = await indexer.reindexChanges(codebaseDir);
518
+
519
+ expect(stats.filesAdded).toBe(0);
520
+ expect(stats.filesModified).toBe(0);
521
+ expect(stats.filesDeleted).toBe(0);
522
+ });
523
+
524
+ it("should call progress callback during reindexing", async () => {
525
+ await createTestFile(
526
+ codebaseDir,
527
+ "test.ts",
528
+ "export const existingValue = 1;\nconsole.log('Existing');"
529
+ );
530
+ await indexer.indexCodebase(codebaseDir);
531
+
532
+ await createTestFile(
533
+ codebaseDir,
534
+ "new.ts",
535
+ "export const newValue = 2;\nconsole.log('New file');"
536
+ );
537
+
538
+ const progressCallback = vi.fn();
539
+ await indexer.reindexChanges(codebaseDir, progressCallback);
540
+
541
+ expect(progressCallback).toHaveBeenCalled();
542
+ });
543
+ });
544
+
545
+ describe("path validation", () => {
546
+ it("should handle non-existent paths gracefully", async () => {
547
+ // Create a path that doesn't exist yet
548
+ const nonExistentDir = join(codebaseDir, "non-existent-dir");
549
+
550
+ // Should not throw error, validatePath falls back to absolute path
551
+ // and scanner finds 0 files
552
+ const stats = await indexer.indexCodebase(nonExistentDir);
553
+ expect(stats.filesScanned).toBe(0);
554
+ expect(stats.status).toBe("completed");
555
+ });
556
+
557
+ it("should resolve real paths for existing directories", async () => {
558
+ await createTestFile(codebaseDir, "test.ts", "export const test = true;");
559
+
560
+ // Should successfully index with real path
561
+ const stats = await indexer.indexCodebase(codebaseDir);
562
+ expect(stats.filesScanned).toBeGreaterThan(0);
563
+ });
564
+ });
565
+
566
+ describe("clearIndex", () => {
567
+ it("should clear indexed codebase", async () => {
568
+ await createTestFile(
569
+ codebaseDir,
570
+ "test.ts",
571
+ "export const configValue = 1;\nconsole.log('Config loaded');"
572
+ );
573
+ await indexer.indexCodebase(codebaseDir);
574
+
575
+ await indexer.clearIndex(codebaseDir);
576
+
577
+ const status = await indexer.getIndexStatus(codebaseDir);
578
+ expect(status.isIndexed).toBe(false);
579
+ });
580
+
581
+ it("should handle clearing non-indexed codebase", async () => {
582
+ await expect(indexer.clearIndex(codebaseDir)).resolves.not.toThrow();
583
+ });
584
+
585
+ it("should allow re-indexing after clearing", async () => {
586
+ await createTestFile(
587
+ codebaseDir,
588
+ "test.ts",
589
+ "export const reindexValue = 1;\nconsole.log('Reindexing');"
590
+ );
591
+ await indexer.indexCodebase(codebaseDir);
592
+
593
+ await indexer.clearIndex(codebaseDir);
594
+
595
+ const stats = await indexer.indexCodebase(codebaseDir);
596
+ expect(stats.status).toBe("completed");
597
+ });
598
+ });
599
+
600
+ describe("edge cases", () => {
601
+ it("should handle nested directory structures", async () => {
602
+ await fs.mkdir(join(codebaseDir, "src", "components"), { recursive: true });
603
+ await createTestFile(
604
+ codebaseDir,
605
+ "src/components/Button.ts",
606
+ `export const Button = () => {
607
+ console.log('Button component rendering');
608
+ const handleClick = () => {
609
+ console.log('Button clicked');
610
+ };
611
+ return '<button>Click me</button>';
612
+ }`
613
+ );
614
+
615
+ const stats = await indexer.indexCodebase(codebaseDir);
616
+
617
+ expect(stats.filesIndexed).toBe(1);
618
+ });
619
+
620
+ it("should handle files with unicode content", async () => {
621
+ await createTestFile(codebaseDir, "test.ts", "const greeting = '你好世界';");
622
+
623
+ const stats = await indexer.indexCodebase(codebaseDir);
624
+
625
+ expect(stats.status).toBe("completed");
626
+ });
627
+
628
+ it("should handle very large files", async () => {
629
+ const largeContent = "function test() {}\n".repeat(1000);
630
+ await createTestFile(codebaseDir, "large.ts", largeContent);
631
+
632
+ const stats = await indexer.indexCodebase(codebaseDir);
633
+
634
+ expect(stats.chunksCreated).toBeGreaterThan(1);
635
+ });
636
+
637
+ it("should generate consistent collection names", async () => {
638
+ await createTestFile(codebaseDir, "test.ts", "const x = 1;");
639
+
640
+ await indexer.indexCodebase(codebaseDir);
641
+ const status1 = await indexer.getIndexStatus(codebaseDir);
642
+
643
+ await indexer.clearIndex(codebaseDir);
644
+
645
+ await indexer.indexCodebase(codebaseDir);
646
+ const status2 = await indexer.getIndexStatus(codebaseDir);
647
+
648
+ expect(status1.collectionName).toBe(status2.collectionName);
649
+ });
650
+
651
+ it("should handle concurrent operations gracefully", async () => {
652
+ await createTestFile(codebaseDir, "test.ts", "const x = 1;");
653
+
654
+ await indexer.indexCodebase(codebaseDir);
655
+
656
+ const searchPromises = [
657
+ indexer.searchCode(codebaseDir, "test"),
658
+ indexer.searchCode(codebaseDir, "const"),
659
+ indexer.getIndexStatus(codebaseDir),
660
+ ];
661
+
662
+ const results = await Promise.all(searchPromises);
663
+
664
+ expect(results).toHaveLength(3);
665
+ });
666
+ });
667
+
668
+ describe("Chunk limiting configuration", () => {
669
+ it("should respect maxChunksPerFile limit", async () => {
670
+ const limitedConfig = {
671
+ ...config,
672
+ maxChunksPerFile: 2,
673
+ };
674
+ const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
675
+
676
+ // Create a large file that would generate many chunks
677
+ const largeContent = Array(50)
678
+ .fill(null)
679
+ .map((_, i) => `function test${i}() { console.log('test ${i}'); return ${i}; }`)
680
+ .join("\n\n");
681
+
682
+ await createTestFile(codebaseDir, "large.ts", largeContent);
683
+
684
+ const stats = await limitedIndexer.indexCodebase(codebaseDir);
685
+
686
+ // Should limit chunks per file to 2
687
+ expect(stats.chunksCreated).toBeLessThanOrEqual(2);
688
+ expect(stats.filesIndexed).toBe(1);
689
+ });
690
+
691
+ it("should respect maxTotalChunks limit", async () => {
692
+ const limitedConfig = {
693
+ ...config,
694
+ maxTotalChunks: 3,
695
+ };
696
+ const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
697
+
698
+ // Create multiple files
699
+ for (let i = 0; i < 10; i++) {
700
+ await createTestFile(
701
+ codebaseDir,
702
+ `file${i}.ts`,
703
+ `export function func${i}() { console.log('function ${i}'); return ${i}; }`
704
+ );
705
+ }
706
+
707
+ const stats = await limitedIndexer.indexCodebase(codebaseDir);
708
+
709
+ // Should stop after reaching max total chunks
710
+ expect(stats.chunksCreated).toBeLessThanOrEqual(3);
711
+ expect(stats.filesIndexed).toBeGreaterThan(0);
712
+ });
713
+
714
+ it("should handle maxTotalChunks during chunk iteration", async () => {
715
+ const limitedConfig = {
716
+ ...config,
717
+ maxTotalChunks: 1,
718
+ };
719
+ const limitedIndexer = new CodeIndexer(qdrant as any, embeddings, limitedConfig);
720
+
721
+ // Create a file with multiple chunks
722
+ const content = `
723
+ function first() {
724
+ console.log('first function');
725
+ return 1;
726
+ }
727
+
728
+ function second() {
729
+ console.log('second function');
730
+ return 2;
731
+ }
732
+
733
+ function third() {
734
+ console.log('third function');
735
+ return 3;
736
+ }
737
+ `;
738
+
739
+ await createTestFile(codebaseDir, "multi.ts", content);
740
+
741
+ const stats = await limitedIndexer.indexCodebase(codebaseDir);
742
+
743
+ // Should stop after first chunk
744
+ expect(stats.chunksCreated).toBe(1);
745
+ });
746
+ });
747
+
748
+ describe("Progress callback coverage", () => {
749
+ it("should call progress callback during reindexChanges", async () => {
750
+ // Initial indexing
751
+ await createTestFile(
752
+ codebaseDir,
753
+ "file1.ts",
754
+ "export const initial = 1;\nconsole.log('Initial');"
755
+ );
756
+ await indexer.indexCodebase(codebaseDir);
757
+
758
+ // Add new file
759
+ await createTestFile(
760
+ codebaseDir,
761
+ "file2.ts",
762
+ `export const added = 2;
763
+ console.log('Added file');
764
+
765
+ export function process() {
766
+ console.log('Processing');
767
+ return true;
768
+ }`
769
+ );
770
+
771
+ const progressUpdates: string[] = [];
772
+ const progressCallback = (progress: any) => {
773
+ progressUpdates.push(progress.phase);
774
+ };
775
+
776
+ await indexer.reindexChanges(codebaseDir, progressCallback);
777
+
778
+ // Should have called progress callback with various phases
779
+ expect(progressUpdates.length).toBeGreaterThan(0);
780
+ expect(progressUpdates).toContain("scanning");
781
+ });
782
+ });
783
+
784
+ describe("Error handling edge cases", () => {
785
+ it("should handle non-Error exceptions", async () => {
786
+ // This tests the `error instanceof Error ? error.message : String(error)` branch
787
+ await createTestFile(codebaseDir, "test.ts", "const x = 1;");
788
+
789
+ // Mock fs.readFile to throw a non-Error object
790
+ const originalReadFile = fs.readFile;
791
+ let callCount = 0;
792
+
793
+ // @ts-expect-error - Mocking for test
794
+ fs.readFile = async (path: any, encoding: any) => {
795
+ callCount++;
796
+ if (callCount === 1 && typeof path === "string" && path.endsWith("test.ts")) {
797
+ // Throw a non-Error object
798
+ throw "String error";
799
+ }
800
+ return originalReadFile(path, encoding);
801
+ };
802
+
803
+ try {
804
+ const stats = await indexer.indexCodebase(codebaseDir);
805
+
806
+ // Should handle the error gracefully
807
+ expect(stats.status).toBe("completed");
808
+ expect(stats.errors?.some((e) => e.includes("String error"))).toBe(true);
809
+ } finally {
810
+ // Restore original function
811
+ // @ts-expect-error
812
+ fs.readFile = originalReadFile;
813
+ }
814
+ });
815
+ });
816
+ });
817
+
818
+ // Helper function to create test files
819
+ async function createTestFile(
820
+ baseDir: string,
821
+ relativePath: string,
822
+ content: string
823
+ ): Promise<void> {
824
+ const fullPath = join(baseDir, relativePath);
825
+ const dir = join(fullPath, "..");
826
+ await fs.mkdir(dir, { recursive: true });
827
+ await fs.writeFile(fullPath, content, "utf-8");
828
+ }