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