UNPKG

mcp-server-debug-thinking

Version:

Graph-based MCP server for systematic debugging using Problem-Solution Trees and Hypothesis-Experiment-Learning cycles

348 lines 14.8 kB
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { GraphStorage } from "../../services/GraphStorage.js"; import { ensureDirectory } from "../../utils/storage.js"; import fs from "fs/promises"; import path from "path"; describe("GraphStorage", () => { let storage; const testDataDir = ".debug-thinking-mcp-test"; beforeEach(async () => { // Set test data directory process.env.DEBUG_DATA_DIR = testDataDir; storage = new GraphStorage(); await storage.initialize(); }); afterEach(async () => { // Clean up test files try { // Remove the entire test directory await fs.rm(testDataDir, { recursive: true, force: true }); } catch (_error) { // Directory might not exist } // Clean up environment variable delete process.env.DEBUG_DATA_DIR; }); describe("initialize", () => { it("should create data directory", async () => { const actualDir = path.join(testDataDir, ".debug-thinking-mcp"); const dirExists = await fs .access(actualDir) .then(() => true) .catch(() => false); expect(dirExists).toBe(true); }); }); describe("saveNode", () => { it("should save a node to JSONL file", async () => { const node = { id: "test-node-1", type: "problem", content: "Test problem", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: ["test"], status: "open", }, }; await storage.saveNode(node); const nodesFile = path.join(testDataDir, ".debug-thinking-mcp", "nodes.jsonl"); const fileContent = await fs.readFile(nodesFile, "utf-8"); const savedNode = JSON.parse(fileContent.trim()); expect(savedNode.id).toBe(node.id); expect(savedNode.type).toBe(node.type); expect(savedNode.content).toBe(node.content); expect(savedNode.metadata.tags).toEqual(node.metadata.tags); }); it("should append multiple nodes", async () => { const node1 = { id: "node-1", type: "problem", content: "Problem 1", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: [], }, }; const node2 = { id: "node-2", type: "hypothesis", content: "Hypothesis 1", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: [], confidence: 80, }, }; await storage.saveNode(node1); await storage.saveNode(node2); const nodesFile = path.join(testDataDir, ".debug-thinking-mcp", "nodes.jsonl"); const fileContent = await fs.readFile(nodesFile, "utf-8"); const lines = fileContent.trim().split("\n"); expect(lines).toHaveLength(2); expect(JSON.parse(lines[0]).id).toBe("node-1"); expect(JSON.parse(lines[1]).id).toBe("node-2"); }); }); describe("saveEdge", () => { it("should save an edge to JSONL file", async () => { const edge = { id: "edge-1", type: "hypothesizes", from: "node-1", to: "node-2", strength: 0.8, metadata: { createdAt: new Date(), reasoning: "Test reasoning", }, }; await storage.saveEdge(edge); const edgesFile = path.join(testDataDir, ".debug-thinking-mcp", "edges.jsonl"); const fileContent = await fs.readFile(edgesFile, "utf-8"); const savedEdge = JSON.parse(fileContent.trim()); expect(savedEdge.id).toBe(edge.id); expect(savedEdge.type).toBe(edge.type); expect(savedEdge.from).toBe(edge.from); expect(savedEdge.to).toBe(edge.to); expect(savedEdge.strength).toBe(edge.strength); }); }); describe("saveGraphMetadata", () => { it("should save graph metadata as JSON", async () => { const graph = { nodes: new Map([ [ "n1", { id: "n1", type: "problem", content: "Test", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: [] }, }, ], ]), edges: new Map([["e1", { id: "e1", type: "supports", from: "n1", to: "n2", strength: 1 }]]), roots: ["n1"], metadata: { createdAt: new Date(), lastModified: new Date(), sessionCount: 5, }, }; await storage.saveGraphMetadata(graph); const metadataFile = path.join(testDataDir, ".debug-thinking-mcp", "graph-metadata.json"); const fileContent = await fs.readFile(metadataFile, "utf-8"); const savedMetadata = JSON.parse(fileContent); expect(savedMetadata.roots).toEqual(["n1"]); expect(savedMetadata.sessionCount).toBe(5); expect(savedMetadata.nodeCount).toBe(1); expect(savedMetadata.edgeCount).toBe(1); }); }); describe("loadGraph", () => { it("should return null when no data exists", async () => { const graph = await storage.loadGraph(); expect(graph).toBeNull(); }); it("should load graph from saved files", async () => { // Save some data first const node1 = { id: "node-1", type: "problem", content: "Saved problem", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: ["saved"], }, }; const edge1 = { id: "edge-1", type: "decomposes", from: "node-1", to: "node-2", strength: 0.9, }; const graph = { nodes: new Map([["node-1", node1]]), edges: new Map([["edge-1", edge1]]), roots: ["node-1"], metadata: { createdAt: new Date(), lastModified: new Date(), sessionCount: 1, }, }; await storage.saveNode(node1); await storage.saveEdge(edge1); await storage.saveGraphMetadata(graph); // Load the graph const loadedGraph = await storage.loadGraph(); expect(loadedGraph).not.toBeNull(); expect(loadedGraph?.nodes.size).toBe(1); expect(loadedGraph?.edges.size).toBe(1); expect(loadedGraph?.roots).toEqual(["node-1"]); const loadedNode = loadedGraph?.nodes.get("node-1"); expect(loadedNode?.content).toBe("Saved problem"); expect(loadedNode?.metadata.tags).toEqual(["saved"]); const loadedEdge = loadedGraph?.edges.get("edge-1"); expect(loadedEdge?.type).toBe("decomposes"); expect(loadedEdge?.strength).toBe(0.9); }); it("should handle duplicate nodes by keeping the latest", async () => { const nodesFile = path.join(testDataDir, ".debug-thinking-mcp", "nodes.jsonl"); // Create directory first await fs.mkdir(testDataDir, { recursive: true }); // Write duplicate nodes const node1v1 = { id: "node-1", type: "problem", content: "Version 1", metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: [], }, }; const node1v2 = { id: "node-1", type: "problem", content: "Version 2", metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: [], }, }; await fs.writeFile(nodesFile, `${JSON.stringify(node1v1)}\n${JSON.stringify(node1v2)}\n`); const loadedGraph = await storage.loadGraph(); expect(loadedGraph).not.toBeNull(); expect(loadedGraph?.nodes.size).toBe(1); const loadedNode = loadedGraph?.nodes.get("node-1"); expect(loadedNode?.content).toBe("Version 2"); // Latest version }); it("should handle malformed data gracefully", async () => { const metadataFile = path.join(testDataDir, ".debug-thinking-mcp", "graph-metadata.json"); // Create directory and write invalid JSON await fs.mkdir(testDataDir, { recursive: true }); await fs.writeFile(metadataFile, "invalid json data"); // Should not throw, just return default metadata const graph = await storage.loadGraph(); expect(graph).not.toBeNull(); expect(graph?.metadata.sessionCount).toBe(0); }); }); describe("error handling", () => { it("should handle save errors gracefully", async () => { const node = { id: "test-node", type: "problem", content: "Test", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: [], }, }; // Mock file system error on appendFile (used by appendJsonLine) const originalAppendFile = fs.appendFile; vi.spyOn(fs, "appendFile").mockRejectedValueOnce(new Error("Write failed")); // The error is thrown by saveNode await expect(storage.saveNode(node)).rejects.toThrow("Write failed"); // Verify the mock was called expect(vi.mocked(fs.appendFile)).toHaveBeenCalled(); vi.spyOn(fs, "appendFile").mockImplementation(originalAppendFile); }); it("should handle load errors gracefully", async () => { // Create files that exist but fail to parse await ensureDirectory(path.join(testDataDir, ".debug-thinking-mcp")); const metadataFile = path.join(testDataDir, ".debug-thinking-mcp", "graph-metadata.json"); // Write invalid JSON to metadata file await fs.writeFile(metadataFile, "{ invalid json"); // This should trigger the error handling in loadGraph but still return a valid graph const result = await storage.loadGraph(); expect(result).not.toBeNull(); // The graph should have default metadata due to error handling expect(result?.metadata.sessionCount).toBe(0); }); it("should handle edge metadata parsing", async () => { // Save an edge with metadata const edge = { id: "edge-with-meta", type: "supports", from: "n1", to: "n2", strength: 0.9, metadata: { createdAt: new Date(), reason: "test reason", }, }; await storage.saveEdge(edge); // Load and verify metadata is preserved const graph = await storage.loadGraph(); expect(graph).not.toBeNull(); const loadedEdge = graph?.edges.get("edge-with-meta"); expect(loadedEdge?.metadata).toBeDefined(); expect(loadedEdge?.metadata?.reason).toBe("test reason"); }); }); describe("clearStorage", () => { it("should have clearStorage method", () => { expect(storage.clearStorage).toBeDefined(); expect(typeof storage.clearStorage).toBe("function"); }); it("should be able to call clearStorage", async () => { // Just verify it doesn't throw await expect(storage.clearStorage()).resolves.not.toThrow(); }); }); describe("error handling in saveGraphMetadata", () => { it("should throw error when writeJsonFile fails", async () => { const graph = { nodes: new Map([ [ "n1", { id: "n1", type: "problem", content: "Test", metadata: { createdAt: new Date(), updatedAt: new Date(), tags: [] }, }, ], ]), edges: new Map(), roots: ["n1"], metadata: { createdAt: new Date(), lastModified: new Date(), sessionCount: 1, }, }; // Mock writeJsonFile to throw error const originalWriteFile = fs.writeFile; vi.spyOn(fs, "writeFile").mockRejectedValueOnce(new Error("Write failed")); await expect(storage.saveGraphMetadata(graph)).rejects.toThrow("Write failed"); vi.spyOn(fs, "writeFile").mockImplementation(originalWriteFile); }); }); describe("error handling in loadGraph", () => { it.skip("should return null and log error when loading fails", async () => { // Mock file exists but readFile fails vi.spyOn(fs, "access").mockResolvedValueOnce(undefined); // nodes.jsonl exists vi.spyOn(fs, "readFile").mockRejectedValueOnce(new Error("Read failed")); const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { }); const result = await storage.loadGraph(); expect(result).toBeNull(); // Verify error was logged expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to load graph:"), expect.any(Error)); consoleSpy.mockRestore(); }); }); }); //# sourceMappingURL=GraphStorage.test.js.map