Skip to main content
Glama
snapshot.test.ts11.3 kB
import { promises as fs } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { MerkleTree } from "../../../src/code/sync/merkle.js"; import { SnapshotManager } from "../../../src/code/sync/snapshot.js"; describe("SnapshotManager", () => { let tempDir: string; let snapshotManager: SnapshotManager; let collectionName: string; beforeEach(async () => { // Create a temporary directory for test snapshots tempDir = join( tmpdir(), `qdrant-mcp-test-${Date.now()}-${Math.random().toString(36).substring(7)}` ); await fs.mkdir(tempDir, { recursive: true }); collectionName = "test-collection"; const snapshotPath = join(tempDir, `${collectionName}.json`); snapshotManager = new SnapshotManager(snapshotPath); }); afterEach(async () => { // Clean up temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (_error) { // Ignore cleanup errors } }); describe("save", () => { it("should save snapshot with file hashes and merkle tree", async () => { const fileHashes = new Map([ ["file1.ts", "hash1"], ["file2.ts", "hash2"], ]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const snapshotPath = join(tempDir, `${collectionName}.json`); const exists = await fs .access(snapshotPath) .then(() => true) .catch(() => false); expect(exists).toBe(true); }); it("should save correct snapshot data", async () => { const fileHashes = new Map([ ["file1.ts", "hash1"], ["file2.ts", "hash2"], ]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded).toBeDefined(); expect(loaded?.codebasePath).toBe("/test/codebase"); expect(loaded?.fileHashes.size).toBe(2); expect(loaded?.fileHashes.get("file1.ts")).toBe("hash1"); expect(loaded?.fileHashes.get("file2.ts")).toBe("hash2"); }); it("should overwrite existing snapshot", async () => { const fileHashes1 = new Map([["file1.ts", "hash1"]]); const tree1 = new MerkleTree(); tree1.build(fileHashes1); await snapshotManager.save("/test/codebase1", fileHashes1, tree1); const fileHashes2 = new Map([["file2.ts", "hash2"]]); const tree2 = new MerkleTree(); tree2.build(fileHashes2); await snapshotManager.save("/test/codebase2", fileHashes2, tree2); const loaded = await snapshotManager.load(); expect(loaded?.codebasePath).toBe("/test/codebase2"); expect(loaded?.fileHashes.size).toBe(1); expect(loaded?.fileHashes.get("file2.ts")).toBe("hash2"); }); it("should create snapshot directory if it doesn't exist", async () => { const nonExistentDir = join(tempDir, "nested", "path"); const snapshotPath = join(nonExistentDir, `${collectionName}.json`); const manager = new SnapshotManager(snapshotPath); const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await manager.save("/test/codebase", fileHashes, tree); const exists = await fs .access(snapshotPath) .then(() => true) .catch(() => false); expect(exists).toBe(true); }); it("should handle empty file hashes", async () => { const fileHashes = new Map<string, string>(); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded?.fileHashes.size).toBe(0); }); it("should preserve timestamp", async () => { const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); const beforeSave = Date.now(); await snapshotManager.save("/test/codebase", fileHashes, tree); const afterSave = Date.now(); const loaded = await snapshotManager.load(); expect(loaded?.timestamp).toBeGreaterThanOrEqual(beforeSave); expect(loaded?.timestamp).toBeLessThanOrEqual(afterSave); }); }); describe("load", () => { it("should return null if snapshot doesn't exist", async () => { const loaded = await snapshotManager.load(); expect(loaded).toBeNull(); }); it("should load previously saved snapshot", async () => { const fileHashes = new Map([ ["file1.ts", "hash1"], ["file2.ts", "hash2"], ["file3.ts", "hash3"], ]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded).toBeDefined(); expect(loaded?.codebasePath).toBe("/test/codebase"); expect(loaded?.fileHashes.size).toBe(3); expect(loaded?.merkleTree.getRootHash()).toBe(tree.getRootHash()); }); it("should handle corrupted snapshot file", async () => { const snapshotPath = join(tempDir, `${collectionName}.json`); await fs.writeFile(snapshotPath, "{ invalid json", "utf-8"); const loaded = await snapshotManager.load(); expect(loaded).toBeNull(); }); it("should handle missing fields in snapshot", async () => { const snapshotPath = join(tempDir, `${collectionName}.json`); await fs.writeFile(snapshotPath, JSON.stringify({ codebasePath: "/test" }), "utf-8"); const loaded = await snapshotManager.load(); // Should handle gracefully, might return null or partial data expect(loaded).toBeDefined(); }); it("should reconstruct merkle tree from serialized data", async () => { const fileHashes = new Map([ ["file1.ts", "hash1"], ["file2.ts", "hash2"], ]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded?.merkleTree).toBeDefined(); expect(loaded?.merkleTree.getRootHash()).toBe(tree.getRootHash()); }); }); describe("exists", () => { it("should return false when snapshot doesn't exist", async () => { const exists = await snapshotManager.exists(); expect(exists).toBe(false); }); it("should return true when snapshot exists", async () => { const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const exists = await snapshotManager.exists(); expect(exists).toBe(true); }); }); describe("delete", () => { it("should delete existing snapshot", async () => { const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); expect(await snapshotManager.exists()).toBe(true); await snapshotManager.delete(); expect(await snapshotManager.exists()).toBe(false); }); it("should handle deleting non-existent snapshot", async () => { await expect(snapshotManager.delete()).resolves.not.toThrow(); }); it("should allow saving after deletion", async () => { const fileHashes1 = new Map([["file1.ts", "hash1"]]); const tree1 = new MerkleTree(); tree1.build(fileHashes1); await snapshotManager.save("/test/codebase1", fileHashes1, tree1); await snapshotManager.delete(); const fileHashes2 = new Map([["file2.ts", "hash2"]]); const tree2 = new MerkleTree(); tree2.build(fileHashes2); await snapshotManager.save("/test/codebase2", fileHashes2, tree2); const loaded = await snapshotManager.load(); expect(loaded?.codebasePath).toBe("/test/codebase2"); }); }); describe("edge cases", () => { it("should handle large number of files", async () => { const fileHashes = new Map<string, string>(); for (let i = 0; i < 1000; i++) { fileHashes.set(`file${i}.ts`, `hash${i}`); } const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded?.fileHashes.size).toBe(1000); }); it("should handle special characters in paths", async () => { const fileHashes = new Map([ ["/path/with spaces/file.ts", "hash1"], ["/path/with-dashes/file.ts", "hash2"], ["/path/with_underscores/file.ts", "hash3"], ]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded?.fileHashes.size).toBe(3); }); it("should handle collection names with special characters", async () => { const specialName = "collection-with-dashes_and_underscores"; const snapshotPath = join(tempDir, `${specialName}.json`); const manager = new SnapshotManager(snapshotPath); const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await manager.save("/test/codebase", fileHashes, tree); const loaded = await manager.load(); expect(loaded).toBeDefined(); }); it("should handle very long codebase paths", async () => { const longPath = `${"/very/long/path/".repeat(50)}codebase`; const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save(longPath, fileHashes, tree); const loaded = await snapshotManager.load(); expect(loaded?.codebasePath).toBe(longPath); }); }); describe("concurrent operations", () => { it("should handle rapid save operations", async () => { const promises = []; for (let i = 0; i < 10; i++) { const fileHashes = new Map([[`file${i}.ts`, `hash${i}`]]); const tree = new MerkleTree(); tree.build(fileHashes); promises.push(snapshotManager.save(`/test/codebase${i}`, fileHashes, tree)); } await Promise.all(promises); // Last save should win const loaded = await snapshotManager.load(); expect(loaded).toBeDefined(); }); it("should handle concurrent save and load", async () => { const fileHashes = new Map([["file.ts", "hash"]]); const tree = new MerkleTree(); tree.build(fileHashes); await snapshotManager.save("/test/codebase", fileHashes, tree); const promises = [snapshotManager.load(), snapshotManager.load(), snapshotManager.load()]; const results = await Promise.all(promises); results.forEach((result) => { expect(result).toBeDefined(); }); }); }); });

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