@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.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +14 -2
- package/README.md +3 -2
- package/build/code/indexer.d.ts +5 -0
- package/build/code/indexer.d.ts.map +1 -1
- package/build/code/indexer.js +116 -14
- package/build/code/indexer.js.map +1 -1
- package/build/code/types.d.ts +4 -0
- package/build/code/types.d.ts.map +1 -1
- package/build/index.js +28 -831
- package/build/index.js.map +1 -1
- package/build/prompts/register.d.ts +10 -0
- package/build/prompts/register.d.ts.map +1 -0
- package/build/prompts/register.js +50 -0
- package/build/prompts/register.js.map +1 -0
- package/build/resources/index.d.ts +10 -0
- package/build/resources/index.d.ts.map +1 -0
- package/build/resources/index.js +60 -0
- package/build/resources/index.js.map +1 -0
- package/build/tools/code.d.ts +10 -0
- package/build/tools/code.d.ts.map +1 -0
- package/build/tools/code.js +132 -0
- package/build/tools/code.js.map +1 -0
- package/build/tools/collection.d.ts +12 -0
- package/build/tools/collection.d.ts.map +1 -0
- package/build/tools/collection.js +59 -0
- package/build/tools/collection.js.map +1 -0
- package/build/tools/document.d.ts +12 -0
- package/build/tools/document.d.ts.map +1 -0
- package/build/tools/document.js +84 -0
- package/build/tools/document.js.map +1 -0
- package/build/tools/index.d.ts +18 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +30 -0
- package/build/tools/index.js.map +1 -0
- package/build/tools/schemas.d.ts +75 -0
- package/build/tools/schemas.d.ts.map +1 -0
- package/build/tools/schemas.js +114 -0
- package/build/tools/schemas.js.map +1 -0
- package/build/tools/search.d.ts +12 -0
- package/build/tools/search.d.ts.map +1 -0
- package/build/tools/search.js +79 -0
- package/build/tools/search.js.map +1 -0
- package/examples/code-search/README.md +19 -4
- package/package.json +1 -1
- package/src/code/indexer.ts +186 -38
- package/src/code/types.ts +5 -0
- package/src/index.ts +26 -983
- package/src/prompts/register.ts +71 -0
- package/src/resources/index.ts +79 -0
- package/src/tools/code.ts +195 -0
- package/src/tools/collection.ts +100 -0
- package/src/tools/document.ts +113 -0
- package/src/tools/index.ts +48 -0
- package/src/tools/schemas.ts +130 -0
- package/src/tools/search.ts +122 -0
- package/tests/code/indexer.test.ts +412 -74
- 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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
-
return
|
|
107
|
+
getModel(): string {
|
|
108
|
+
return "mock-model";
|
|
88
109
|
}
|
|
89
110
|
|
|
90
|
-
async
|
|
91
|
-
|
|
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(
|
|
219
|
-
|
|
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(
|
|
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(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
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(
|
|
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", {
|
|
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(
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
496
|
+
const status = await indexer.getIndexStatus(codebaseDir);
|
|
418
497
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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(
|
|
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"), {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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, "..");
|