Skip to main content
Glama
synchronizer.test.ts17.1 kB
import { promises as fs } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { FileSynchronizer } from "../../../src/code/sync/synchronizer.js"; describe("FileSynchronizer", () => { let tempDir: string; let codebaseDir: string; let synchronizer: FileSynchronizer; let collectionName: string; beforeEach(async () => { // Create temporary directories for testing tempDir = join( tmpdir(), `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}` ); codebaseDir = join(tempDir, "codebase"); await fs.mkdir(codebaseDir, { recursive: true }); // Use unique collection name to avoid conflicts between test runs collectionName = `test-collection-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; synchronizer = new FileSynchronizer(codebaseDir, collectionName); }); afterEach(async () => { // Clean up temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (_error) { // Ignore cleanup errors } // Clean up snapshot file try { const snapshotPath = join(homedir(), ".qdrant-mcp", "snapshots", `${collectionName}.json`); await fs.rm(snapshotPath, { force: true }); } catch (_error) { // Ignore cleanup errors } }); describe("initialize", () => { it("should return false when no snapshot exists", async () => { const hasSnapshot = await synchronizer.initialize(); expect(hasSnapshot).toBe(false); }); it("should return true when snapshot exists", async () => { // Create some files and update snapshot await createFile(codebaseDir, "file1.ts", "content1"); await synchronizer.updateSnapshot(["file1.ts"]); // Create a new synchronizer and initialize const newSync = new FileSynchronizer(codebaseDir, collectionName); const hasSnapshot = await newSync.initialize(); expect(hasSnapshot).toBe(true); }); it("should load previous file hashes", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); const newSync = new FileSynchronizer(codebaseDir, collectionName); await newSync.initialize(); const changes = await newSync.detectChanges(["file1.ts", "file2.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); }); describe("updateSnapshot", () => { it("should save snapshot with file hashes", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); const newSync = new FileSynchronizer(codebaseDir, collectionName, tempDir); const hasSnapshot = await newSync.initialize(); expect(hasSnapshot).toBe(true); }); it("should handle empty file list", async () => { await synchronizer.updateSnapshot([]); const newSync = new FileSynchronizer(codebaseDir, collectionName); const hasSnapshot = await newSync.initialize(); expect(hasSnapshot).toBe(true); }); it("should overwrite previous snapshot", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await synchronizer.updateSnapshot(["file1.ts"]); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file2.ts"]); const newSync = new FileSynchronizer(codebaseDir, collectionName); await newSync.initialize(); const changes = await newSync.detectChanges(["file2.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); it("should handle relative and absolute paths", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); await synchronizer.updateSnapshot([join(codebaseDir, "file.ts")]); // Both should work without duplicates const newSync = new FileSynchronizer(codebaseDir, collectionName); await newSync.initialize(); const changes = await newSync.detectChanges(["file.ts"]); expect(changes.modified).toEqual([]); }); }); describe("detectChanges", () => { it("should detect added files", async () => { await synchronizer.initialize(); await synchronizer.updateSnapshot([]); await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]); expect(changes.added).toContain("file1.ts"); expect(changes.added).toContain("file2.ts"); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); it("should detect deleted files", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); await fs.unlink(join(codebaseDir, "file2.ts")); const changes = await synchronizer.detectChanges(["file1.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual(["file2.ts"]); }); it("should detect modified files", async () => { await createFile(codebaseDir, "file1.ts", "original content"); await synchronizer.updateSnapshot(["file1.ts"]); await createFile(codebaseDir, "file1.ts", "modified content"); const changes = await synchronizer.detectChanges(["file1.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual(["file1.ts"]); expect(changes.deleted).toEqual([]); }); it("should detect mixed changes", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); // Modify file1 await createFile(codebaseDir, "file1.ts", "modified content1"); // Add file3 await createFile(codebaseDir, "file3.ts", "content3"); // Delete file2 const changes = await synchronizer.detectChanges(["file1.ts", "file3.ts"]); expect(changes.added).toEqual(["file3.ts"]); expect(changes.modified).toEqual(["file1.ts"]); expect(changes.deleted).toEqual(["file2.ts"]); }); it("should detect no changes when files unchanged", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await synchronizer.updateSnapshot(["file1.ts"]); const changes = await synchronizer.detectChanges(["file1.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); it("should consider all files as added when no snapshot exists", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]); expect(changes.added).toContain("file1.ts"); expect(changes.added).toContain("file2.ts"); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); }); describe("deleteSnapshot", () => { it("should delete existing snapshot", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); await synchronizer.deleteSnapshot(); const newSync = new FileSynchronizer(codebaseDir, collectionName); const hasSnapshot = await newSync.initialize(); expect(hasSnapshot).toBe(false); }); it("should handle deleting non-existent snapshot", async () => { await expect(synchronizer.deleteSnapshot()).resolves.not.toThrow(); }); it("should allow operations after deletion", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await synchronizer.updateSnapshot(["file1.ts"]); await synchronizer.deleteSnapshot(); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file2.ts"]); const newSync = new FileSynchronizer(codebaseDir, collectionName); const hasSnapshot = await newSync.initialize(); expect(hasSnapshot).toBe(true); }); }); describe("edge cases", () => { it("should handle files with same content different names", async () => { const sameContent = "identical content"; await createFile(codebaseDir, "file1.ts", sameContent); await createFile(codebaseDir, "file2.ts", sameContent); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); const changes = await synchronizer.detectChanges(["file1.ts", "file2.ts"]); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); it("should handle binary files", async () => { const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff]); await fs.writeFile(join(codebaseDir, "binary.dat"), binaryContent); await synchronizer.updateSnapshot(["binary.dat"]); const changes = await synchronizer.detectChanges(["binary.dat"]); expect(changes.modified).toEqual([]); }); it("should handle very large files", async () => { const largeContent = "x".repeat(1024 * 1024); // 1MB await createFile(codebaseDir, "large.ts", largeContent); await synchronizer.updateSnapshot(["large.ts"]); const changes = await synchronizer.detectChanges(["large.ts"]); expect(changes.modified).toEqual([]); }); it("should handle files with special characters in names", async () => { await createFile(codebaseDir, "file with spaces.ts", "content"); await createFile(codebaseDir, "file-with-dashes.ts", "content"); await createFile(codebaseDir, "file_with_underscores.ts", "content"); await synchronizer.updateSnapshot([ "file with spaces.ts", "file-with-dashes.ts", "file_with_underscores.ts", ]); const changes = await synchronizer.detectChanges([ "file with spaces.ts", "file-with-dashes.ts", "file_with_underscores.ts", ]); expect(changes.modified).toEqual([]); }); it("should handle nested directory structures", async () => { await fs.mkdir(join(codebaseDir, "src", "components"), { recursive: true, }); await createFile(codebaseDir, "src/components/Button.tsx", "content"); await synchronizer.updateSnapshot(["src/components/Button.tsx"]); const changes = await synchronizer.detectChanges(["src/components/Button.tsx"]); expect(changes.modified).toEqual([]); }); it("should handle empty files", async () => { await createFile(codebaseDir, "empty.ts", ""); await synchronizer.updateSnapshot(["empty.ts"]); const changes = await synchronizer.detectChanges(["empty.ts"]); expect(changes.modified).toEqual([]); }); it("should detect when empty file becomes non-empty", async () => { await createFile(codebaseDir, "file.ts", ""); await synchronizer.updateSnapshot(["file.ts"]); await createFile(codebaseDir, "file.ts", "now has content"); const changes = await synchronizer.detectChanges(["file.ts"]); expect(changes.modified).toEqual(["file.ts"]); }); it("should handle many files efficiently", async () => { const files: string[] = []; for (let i = 0; i < 100; i++) { const filename = `file${i}.ts`; await createFile(codebaseDir, filename, `content ${i}`); files.push(filename); } await synchronizer.updateSnapshot(files); const changes = await synchronizer.detectChanges(files); expect(changes.added).toEqual([]); expect(changes.modified).toEqual([]); expect(changes.deleted).toEqual([]); }); }); describe("hash calculation", () => { it("should produce same hash for identical content", async () => { const content = "test content"; await createFile(codebaseDir, "file1.ts", content); await synchronizer.updateSnapshot(["file1.ts"]); // Delete and recreate with same content await fs.unlink(join(codebaseDir, "file1.ts")); await createFile(codebaseDir, "file1.ts", content); const changes = await synchronizer.detectChanges(["file1.ts"]); expect(changes.modified).toEqual([]); }); it("should produce different hash for different content", async () => { await createFile(codebaseDir, "file.ts", "original"); await synchronizer.updateSnapshot(["file.ts"]); await createFile(codebaseDir, "file.ts", "modified"); const changes = await synchronizer.detectChanges(["file.ts"]); expect(changes.modified).toEqual(["file.ts"]); }); it("should be sensitive to whitespace changes", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); await createFile(codebaseDir, "file.ts", "content "); const changes = await synchronizer.detectChanges(["file.ts"]); expect(changes.modified).toEqual(["file.ts"]); }); it("should be sensitive to line ending changes", async () => { await createFile(codebaseDir, "file.ts", "line1\nline2"); await synchronizer.updateSnapshot(["file.ts"]); await createFile(codebaseDir, "file.ts", "line1\r\nline2"); const changes = await synchronizer.detectChanges(["file.ts"]); expect(changes.modified).toEqual(["file.ts"]); }); }); describe("getSnapshotAge", () => { it("should return snapshot age in milliseconds", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); // Wait a bit to ensure some time passes await new Promise((resolve) => setTimeout(resolve, 10)); const age = await synchronizer.getSnapshotAge(); expect(age).not.toBeNull(); expect(age).toBeGreaterThanOrEqual(10); }); it("should return null when no snapshot exists", async () => { const age = await synchronizer.getSnapshotAge(); expect(age).toBeNull(); }); it("should return age after snapshot creation", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); const age = await synchronizer.getSnapshotAge(); expect(age).not.toBeNull(); expect(age).toBeGreaterThanOrEqual(0); }); }); describe("needsReindex", () => { it("should return true when no previous tree exists", async () => { await createFile(codebaseDir, "file.ts", "content"); const needsReindex = await synchronizer.needsReindex(["file.ts"]); expect(needsReindex).toBe(true); }); it("should return false when files are unchanged", async () => { await createFile(codebaseDir, "file.ts", "content"); await synchronizer.updateSnapshot(["file.ts"]); const needsReindex = await synchronizer.needsReindex(["file.ts"]); expect(needsReindex).toBe(false); }); it("should return true when files have changed", async () => { await createFile(codebaseDir, "file.ts", "original content"); await synchronizer.updateSnapshot(["file.ts"]); await createFile(codebaseDir, "file.ts", "modified content"); const needsReindex = await synchronizer.needsReindex(["file.ts"]); expect(needsReindex).toBe(true); }); it("should return true when files are added", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await synchronizer.updateSnapshot(["file1.ts"]); await createFile(codebaseDir, "file2.ts", "content2"); const needsReindex = await synchronizer.needsReindex(["file1.ts", "file2.ts"]); expect(needsReindex).toBe(true); }); it("should return true when files are deleted", async () => { await createFile(codebaseDir, "file1.ts", "content1"); await createFile(codebaseDir, "file2.ts", "content2"); await synchronizer.updateSnapshot(["file1.ts", "file2.ts"]); const needsReindex = await synchronizer.needsReindex(["file1.ts"]); expect(needsReindex).toBe(true); }); }); }); // Helper function to create files in the test codebase async function createFile(baseDir: string, relativePath: string, content: string): Promise<void> { const fullPath = join(baseDir, relativePath); const dir = join(fullPath, ".."); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(fullPath, content, "utf-8"); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mhalder/qdrant-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server