@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,501 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { FileSynchronizer } from "../../../src/code/sync/synchronizer.js";
|
|
6
|
+
|
|
7
|
+
describe("FileSynchronizer", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let codebaseDir: string;
|
|
10
|
+
let synchronizer: FileSynchronizer;
|
|
11
|
+
let collectionName: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
// Create temporary directories for testing
|
|
15
|
+
tempDir = join(
|
|
16
|
+
tmpdir(),
|
|
17
|
+
`qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
18
|
+
);
|
|
19
|
+
codebaseDir = join(tempDir, "codebase");
|
|
20
|
+
await fs.mkdir(codebaseDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Use unique collection name to avoid conflicts between test runs
|
|
23
|
+
collectionName = `test-collection-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
24
|
+
synchronizer = new FileSynchronizer(codebaseDir, collectionName);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
// Clean up temporary directory
|
|
29
|
+
try {
|
|
30
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
31
|
+
} catch (_error) {
|
|
32
|
+
// Ignore cleanup errors
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Clean up snapshot file
|
|
36
|
+
try {
|
|
37
|
+
const snapshotPath = join(homedir(), ".qdrant-mcp", "snapshots", `${collectionName}.json`);
|
|
38
|
+
await fs.rm(snapshotPath, { force: true });
|
|
39
|
+
} catch (_error) {
|
|
40
|
+
// Ignore cleanup errors
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("initialize", () => {
|
|
45
|
+
it("should return false when no snapshot exists", async () => {
|
|
46
|
+
const hasSnapshot = await synchronizer.initialize();
|
|
47
|
+
expect(hasSnapshot).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return true when snapshot exists", async () => {
|
|
51
|
+
// Create some files and update snapshot
|
|
52
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
53
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
54
|
+
|
|
55
|
+
// Create a new synchronizer and initialize
|
|
56
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
57
|
+
const hasSnapshot = await newSync.initialize();
|
|
58
|
+
|
|
59
|
+
expect(hasSnapshot).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should load previous file hashes", async () => {
|
|
63
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
64
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
65
|
+
|
|
66
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
67
|
+
|
|
68
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
69
|
+
await newSync.initialize();
|
|
70
|
+
|
|
71
|
+
const changes = await newSync.detectChanges(["file1.ts", "file2.ts"]);
|
|
72
|
+
|
|
73
|
+
expect(changes.added).toEqual([]);
|
|
74
|
+
expect(changes.modified).toEqual([]);
|
|
75
|
+
expect(changes.deleted).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("updateSnapshot", () => {
|
|
80
|
+
it("should save snapshot with file hashes", async () => {
|
|
81
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
82
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
83
|
+
|
|
84
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
85
|
+
|
|
86
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName, tempDir);
|
|
87
|
+
const hasSnapshot = await newSync.initialize();
|
|
88
|
+
|
|
89
|
+
expect(hasSnapshot).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should handle empty file list", async () => {
|
|
93
|
+
await synchronizer.updateSnapshot([]);
|
|
94
|
+
|
|
95
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
96
|
+
const hasSnapshot = await newSync.initialize();
|
|
97
|
+
|
|
98
|
+
expect(hasSnapshot).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should overwrite previous snapshot", async () => {
|
|
102
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
103
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
104
|
+
|
|
105
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
106
|
+
await synchronizer.updateSnapshot(["file2.ts"]);
|
|
107
|
+
|
|
108
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
109
|
+
await newSync.initialize();
|
|
110
|
+
|
|
111
|
+
const changes = await newSync.detectChanges(["file2.ts"]);
|
|
112
|
+
|
|
113
|
+
expect(changes.added).toEqual([]);
|
|
114
|
+
expect(changes.modified).toEqual([]);
|
|
115
|
+
expect(changes.deleted).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle relative and absolute paths", async () => {
|
|
119
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
120
|
+
|
|
121
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
122
|
+
await synchronizer.updateSnapshot([join(codebaseDir, "file.ts")]);
|
|
123
|
+
|
|
124
|
+
// Both should work without duplicates
|
|
125
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
126
|
+
await newSync.initialize();
|
|
127
|
+
|
|
128
|
+
const changes = await newSync.detectChanges(["file.ts"]);
|
|
129
|
+
expect(changes.modified).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("detectChanges", () => {
|
|
134
|
+
it("should detect added files", async () => {
|
|
135
|
+
await synchronizer.initialize();
|
|
136
|
+
await synchronizer.updateSnapshot([]);
|
|
137
|
+
|
|
138
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
139
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
140
|
+
|
|
141
|
+
const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]);
|
|
142
|
+
|
|
143
|
+
expect(changes.added).toContain("file1.ts");
|
|
144
|
+
expect(changes.added).toContain("file2.ts");
|
|
145
|
+
expect(changes.modified).toEqual([]);
|
|
146
|
+
expect(changes.deleted).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should detect deleted files", async () => {
|
|
150
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
151
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
152
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
153
|
+
|
|
154
|
+
await fs.unlink(join(codebaseDir, "file2.ts"));
|
|
155
|
+
|
|
156
|
+
const changes = await synchronizer.detectChanges(["file1.ts"]);
|
|
157
|
+
|
|
158
|
+
expect(changes.added).toEqual([]);
|
|
159
|
+
expect(changes.modified).toEqual([]);
|
|
160
|
+
expect(changes.deleted).toEqual(["file2.ts"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should detect modified files", async () => {
|
|
164
|
+
await createFile(codebaseDir, "file1.ts", "original content");
|
|
165
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
166
|
+
|
|
167
|
+
await createFile(codebaseDir, "file1.ts", "modified content");
|
|
168
|
+
|
|
169
|
+
const changes = await synchronizer.detectChanges(["file1.ts"]);
|
|
170
|
+
|
|
171
|
+
expect(changes.added).toEqual([]);
|
|
172
|
+
expect(changes.modified).toEqual(["file1.ts"]);
|
|
173
|
+
expect(changes.deleted).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should detect mixed changes", async () => {
|
|
177
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
178
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
179
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
180
|
+
|
|
181
|
+
// Modify file1
|
|
182
|
+
await createFile(codebaseDir, "file1.ts", "modified content1");
|
|
183
|
+
|
|
184
|
+
// Add file3
|
|
185
|
+
await createFile(codebaseDir, "file3.ts", "content3");
|
|
186
|
+
|
|
187
|
+
// Delete file2
|
|
188
|
+
|
|
189
|
+
const changes = await synchronizer.detectChanges(["file1.ts", "file3.ts"]);
|
|
190
|
+
|
|
191
|
+
expect(changes.added).toEqual(["file3.ts"]);
|
|
192
|
+
expect(changes.modified).toEqual(["file1.ts"]);
|
|
193
|
+
expect(changes.deleted).toEqual(["file2.ts"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should detect no changes when files unchanged", async () => {
|
|
197
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
198
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
199
|
+
|
|
200
|
+
const changes = await synchronizer.detectChanges(["file1.ts"]);
|
|
201
|
+
|
|
202
|
+
expect(changes.added).toEqual([]);
|
|
203
|
+
expect(changes.modified).toEqual([]);
|
|
204
|
+
expect(changes.deleted).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should consider all files as added when no snapshot exists", async () => {
|
|
208
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
209
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
210
|
+
|
|
211
|
+
const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]);
|
|
212
|
+
|
|
213
|
+
expect(changes.added).toContain("file1.ts");
|
|
214
|
+
expect(changes.added).toContain("file2.ts");
|
|
215
|
+
expect(changes.modified).toEqual([]);
|
|
216
|
+
expect(changes.deleted).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("deleteSnapshot", () => {
|
|
221
|
+
it("should delete existing snapshot", async () => {
|
|
222
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
223
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
224
|
+
|
|
225
|
+
await synchronizer.deleteSnapshot();
|
|
226
|
+
|
|
227
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
228
|
+
const hasSnapshot = await newSync.initialize();
|
|
229
|
+
|
|
230
|
+
expect(hasSnapshot).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle deleting non-existent snapshot", async () => {
|
|
234
|
+
await expect(synchronizer.deleteSnapshot()).resolves.not.toThrow();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should allow operations after deletion", async () => {
|
|
238
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
239
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
240
|
+
|
|
241
|
+
await synchronizer.deleteSnapshot();
|
|
242
|
+
|
|
243
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
244
|
+
await synchronizer.updateSnapshot(["file2.ts"]);
|
|
245
|
+
|
|
246
|
+
const newSync = new FileSynchronizer(codebaseDir, collectionName);
|
|
247
|
+
const hasSnapshot = await newSync.initialize();
|
|
248
|
+
|
|
249
|
+
expect(hasSnapshot).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("edge cases", () => {
|
|
254
|
+
it("should handle files with same content different names", async () => {
|
|
255
|
+
const sameContent = "identical content";
|
|
256
|
+
await createFile(codebaseDir, "file1.ts", sameContent);
|
|
257
|
+
await createFile(codebaseDir, "file2.ts", sameContent);
|
|
258
|
+
|
|
259
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
260
|
+
|
|
261
|
+
const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]);
|
|
262
|
+
|
|
263
|
+
expect(changes.added).toEqual([]);
|
|
264
|
+
expect(changes.modified).toEqual([]);
|
|
265
|
+
expect(changes.deleted).toEqual([]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should handle binary files", async () => {
|
|
269
|
+
const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff]);
|
|
270
|
+
await fs.writeFile(join(codebaseDir, "binary.dat"), binaryContent);
|
|
271
|
+
|
|
272
|
+
await synchronizer.updateSnapshot(["binary.dat"]);
|
|
273
|
+
|
|
274
|
+
const changes = await synchronizer.detectChanges(["binary.dat"]);
|
|
275
|
+
|
|
276
|
+
expect(changes.modified).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should handle very large files", async () => {
|
|
280
|
+
const largeContent = "x".repeat(1024 * 1024); // 1MB
|
|
281
|
+
await createFile(codebaseDir, "large.ts", largeContent);
|
|
282
|
+
|
|
283
|
+
await synchronizer.updateSnapshot(["large.ts"]);
|
|
284
|
+
|
|
285
|
+
const changes = await synchronizer.detectChanges(["large.ts"]);
|
|
286
|
+
|
|
287
|
+
expect(changes.modified).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should handle files with special characters in names", async () => {
|
|
291
|
+
await createFile(codebaseDir, "file with spaces.ts", "content");
|
|
292
|
+
await createFile(codebaseDir, "file-with-dashes.ts", "content");
|
|
293
|
+
await createFile(codebaseDir, "file_with_underscores.ts", "content");
|
|
294
|
+
|
|
295
|
+
await synchronizer.updateSnapshot([
|
|
296
|
+
"file with spaces.ts",
|
|
297
|
+
"file-with-dashes.ts",
|
|
298
|
+
"file_with_underscores.ts",
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const changes = await synchronizer.detectChanges([
|
|
302
|
+
"file with spaces.ts",
|
|
303
|
+
"file-with-dashes.ts",
|
|
304
|
+
"file_with_underscores.ts",
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
expect(changes.modified).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should handle nested directory structures", async () => {
|
|
311
|
+
await fs.mkdir(join(codebaseDir, "src", "components"), {
|
|
312
|
+
recursive: true,
|
|
313
|
+
});
|
|
314
|
+
await createFile(codebaseDir, "src/components/Button.tsx", "content");
|
|
315
|
+
|
|
316
|
+
await synchronizer.updateSnapshot(["src/components/Button.tsx"]);
|
|
317
|
+
|
|
318
|
+
const changes = await synchronizer.detectChanges(["src/components/Button.tsx"]);
|
|
319
|
+
|
|
320
|
+
expect(changes.modified).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should handle empty files", async () => {
|
|
324
|
+
await createFile(codebaseDir, "empty.ts", "");
|
|
325
|
+
|
|
326
|
+
await synchronizer.updateSnapshot(["empty.ts"]);
|
|
327
|
+
|
|
328
|
+
const changes = await synchronizer.detectChanges(["empty.ts"]);
|
|
329
|
+
|
|
330
|
+
expect(changes.modified).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should detect when empty file becomes non-empty", async () => {
|
|
334
|
+
await createFile(codebaseDir, "file.ts", "");
|
|
335
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
336
|
+
|
|
337
|
+
await createFile(codebaseDir, "file.ts", "now has content");
|
|
338
|
+
|
|
339
|
+
const changes = await synchronizer.detectChanges(["file.ts"]);
|
|
340
|
+
|
|
341
|
+
expect(changes.modified).toEqual(["file.ts"]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should handle many files efficiently", async () => {
|
|
345
|
+
const files: string[] = [];
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < 100; i++) {
|
|
348
|
+
const filename = `file${i}.ts`;
|
|
349
|
+
await createFile(codebaseDir, filename, `content ${i}`);
|
|
350
|
+
files.push(filename);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await synchronizer.updateSnapshot(files);
|
|
354
|
+
|
|
355
|
+
const changes = await synchronizer.detectChanges(files);
|
|
356
|
+
|
|
357
|
+
expect(changes.added).toEqual([]);
|
|
358
|
+
expect(changes.modified).toEqual([]);
|
|
359
|
+
expect(changes.deleted).toEqual([]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("hash calculation", () => {
|
|
364
|
+
it("should produce same hash for identical content", async () => {
|
|
365
|
+
const content = "test content";
|
|
366
|
+
await createFile(codebaseDir, "file1.ts", content);
|
|
367
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
368
|
+
|
|
369
|
+
// Delete and recreate with same content
|
|
370
|
+
await fs.unlink(join(codebaseDir, "file1.ts"));
|
|
371
|
+
await createFile(codebaseDir, "file1.ts", content);
|
|
372
|
+
|
|
373
|
+
const changes = await synchronizer.detectChanges(["file1.ts"]);
|
|
374
|
+
|
|
375
|
+
expect(changes.modified).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should produce different hash for different content", async () => {
|
|
379
|
+
await createFile(codebaseDir, "file.ts", "original");
|
|
380
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
381
|
+
|
|
382
|
+
await createFile(codebaseDir, "file.ts", "modified");
|
|
383
|
+
|
|
384
|
+
const changes = await synchronizer.detectChanges(["file.ts"]);
|
|
385
|
+
|
|
386
|
+
expect(changes.modified).toEqual(["file.ts"]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should be sensitive to whitespace changes", async () => {
|
|
390
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
391
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
392
|
+
|
|
393
|
+
await createFile(codebaseDir, "file.ts", "content ");
|
|
394
|
+
|
|
395
|
+
const changes = await synchronizer.detectChanges(["file.ts"]);
|
|
396
|
+
|
|
397
|
+
expect(changes.modified).toEqual(["file.ts"]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should be sensitive to line ending changes", async () => {
|
|
401
|
+
await createFile(codebaseDir, "file.ts", "line1\nline2");
|
|
402
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
403
|
+
|
|
404
|
+
await createFile(codebaseDir, "file.ts", "line1\r\nline2");
|
|
405
|
+
|
|
406
|
+
const changes = await synchronizer.detectChanges(["file.ts"]);
|
|
407
|
+
|
|
408
|
+
expect(changes.modified).toEqual(["file.ts"]);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("getSnapshotAge", () => {
|
|
413
|
+
it("should return snapshot age in milliseconds", async () => {
|
|
414
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
415
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
416
|
+
|
|
417
|
+
// Wait a bit to ensure some time passes
|
|
418
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
419
|
+
|
|
420
|
+
const age = await synchronizer.getSnapshotAge();
|
|
421
|
+
|
|
422
|
+
expect(age).not.toBeNull();
|
|
423
|
+
expect(age).toBeGreaterThanOrEqual(10);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should return null when no snapshot exists", async () => {
|
|
427
|
+
const age = await synchronizer.getSnapshotAge();
|
|
428
|
+
|
|
429
|
+
expect(age).toBeNull();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should return age after snapshot creation", async () => {
|
|
433
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
434
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
435
|
+
|
|
436
|
+
const age = await synchronizer.getSnapshotAge();
|
|
437
|
+
|
|
438
|
+
expect(age).not.toBeNull();
|
|
439
|
+
expect(age).toBeGreaterThanOrEqual(0);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("needsReindex", () => {
|
|
444
|
+
it("should return true when no previous tree exists", async () => {
|
|
445
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
446
|
+
|
|
447
|
+
const needsReindex = await synchronizer.needsReindex(["file.ts"]);
|
|
448
|
+
|
|
449
|
+
expect(needsReindex).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should return false when files are unchanged", async () => {
|
|
453
|
+
await createFile(codebaseDir, "file.ts", "content");
|
|
454
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
455
|
+
|
|
456
|
+
const needsReindex = await synchronizer.needsReindex(["file.ts"]);
|
|
457
|
+
|
|
458
|
+
expect(needsReindex).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should return true when files have changed", async () => {
|
|
462
|
+
await createFile(codebaseDir, "file.ts", "original content");
|
|
463
|
+
await synchronizer.updateSnapshot(["file.ts"]);
|
|
464
|
+
|
|
465
|
+
await createFile(codebaseDir, "file.ts", "modified content");
|
|
466
|
+
|
|
467
|
+
const needsReindex = await synchronizer.needsReindex(["file.ts"]);
|
|
468
|
+
|
|
469
|
+
expect(needsReindex).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should return true when files are added", async () => {
|
|
473
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
474
|
+
await synchronizer.updateSnapshot(["file1.ts"]);
|
|
475
|
+
|
|
476
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
477
|
+
|
|
478
|
+
const needsReindex = await synchronizer.needsReindex(["file1.ts", "file2.ts"]);
|
|
479
|
+
|
|
480
|
+
expect(needsReindex).toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("should return true when files are deleted", async () => {
|
|
484
|
+
await createFile(codebaseDir, "file1.ts", "content1");
|
|
485
|
+
await createFile(codebaseDir, "file2.ts", "content2");
|
|
486
|
+
await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]);
|
|
487
|
+
|
|
488
|
+
const needsReindex = await synchronizer.needsReindex(["file1.ts"]);
|
|
489
|
+
|
|
490
|
+
expect(needsReindex).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Helper function to create files in the test codebase
|
|
496
|
+
async function createFile(baseDir: string, relativePath: string, content: string): Promise<void> {
|
|
497
|
+
const fullPath = join(baseDir, relativePath);
|
|
498
|
+
const dir = join(fullPath, "..");
|
|
499
|
+
await fs.mkdir(dir, { recursive: true });
|
|
500
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
501
|
+
}
|