Skip to main content
Glama
graph-traversal.test.ts20.9 kB
/** * Graph Traversal Integration Tests (Mocked) * * Tests the interaction between GraphTraversal and WaypointBuilder * using mocks for external dependencies (database, embedding storage). * * This is an integration test that verifies internal module interactions work correctly, * NOT a test of external service integration. * * Requirements: 12.2, 12.3, 12.4 */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Link, Memory, Path } from "../../graph/types"; import { LinkType } from "../../graph/types"; // Helper to create mock memory function createMockMemory(id: string, content: string, options: Partial<Memory> = {}): Memory { const now = new Date(); return { id, content, createdAt: options.createdAt ?? now, lastAccessed: options.lastAccessed ?? now, accessCount: options.accessCount ?? 0, salience: options.salience ?? 0.5, strength: options.strength ?? 1.0, userId: options.userId ?? "test-user", sessionId: options.sessionId ?? "test-session", primarySector: options.primarySector ?? "semantic", metadata: options.metadata ?? { keywords: [], tags: [], category: "test", context: "", importance: 0.5, isAtomic: true, }, }; } // Helper to create mock link function createMockLink( sourceId: string, targetId: string, weight: number, linkType: LinkType = LinkType.Semantic ): Link { return { sourceId, targetId, linkType, weight, createdAt: new Date(), traversalCount: 0, }; } describe("GraphTraversal + WaypointBuilder Integration (Mocked)", () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Graph Traversal with Waypoint Links", () => { it("should traverse graph using waypoint links created by WaypointBuilder", () => { // Setup: Simulate a graph with memories connected via waypoint links // Memory A -> Memory B -> Memory C (chain) const memoryA = createMockMemory("memory-A", "First memory in chain"); const memoryB = createMockMemory("memory-B", "Second memory in chain"); const memoryC = createMockMemory("memory-C", "Third memory in chain"); // Links are created but traversal is simulated via hardcoded neighbor logic createMockLink("memory-A", "memory-B", 0.8); createMockLink("memory-B", "memory-C", 0.7); // Simulate BFS traversal const visited = new Set<string>(); const queue: Array<{ memory: Memory; depth: number }> = []; const result: Memory[] = []; // Start at A queue.push({ memory: memoryA, depth: 0 }); visited.add(memoryA.id); // Process queue (simulating BFS) while (queue.length > 0) { const current = queue.shift()!; result.push(current.memory); if (current.depth >= 2) continue; // Get neighbors based on links if (current.memory.id === "memory-A" && !visited.has("memory-B")) { visited.add("memory-B"); queue.push({ memory: memoryB, depth: current.depth + 1 }); } if (current.memory.id === "memory-B" && !visited.has("memory-C")) { visited.add("memory-C"); queue.push({ memory: memoryC, depth: current.depth + 1 }); } } expect(result.length).toBe(3); expect(result[0].id).toBe("memory-A"); expect(result[1].id).toBe("memory-B"); expect(result[2].id).toBe("memory-C"); }); it("should find path through waypoint-connected memories", () => { // Setup: source -> intermediate -> target path const source = createMockMemory("source", "Source memory"); const intermediate = createMockMemory("intermediate", "Intermediate memory"); const target = createMockMemory("target", "Target memory"); const linkSI = createMockLink("source", "intermediate", 0.8); const linkIT = createMockLink("intermediate", "target", 0.7); // Simulate path finding with BFS const visited = new Set<string>(); const queue: Array<{ memory: Memory; path: Memory[]; links: Link[] }> = []; queue.push({ memory: source, path: [source], links: [] }); visited.add(source.id); let foundPath: { memories: Memory[]; links: Link[] } | null = null; while (queue.length > 0 && !foundPath) { const current = queue.shift()!; if (current.memory.id === "target") { foundPath = { memories: current.path, links: current.links }; break; } // Get neighbors if (current.memory.id === "source" && !visited.has("intermediate")) { visited.add("intermediate"); queue.push({ memory: intermediate, path: [...current.path, intermediate], links: [...current.links, linkSI], }); } if (current.memory.id === "intermediate" && !visited.has("target")) { visited.add("target"); queue.push({ memory: target, path: [...current.path, target], links: [...current.links, linkIT], }); } } expect(foundPath).not.toBeNull(); expect(foundPath!.memories.length).toBe(3); expect(foundPath!.links.length).toBe(2); expect(foundPath!.memories[0].id).toBe("source"); expect(foundPath!.memories[1].id).toBe("intermediate"); expect(foundPath!.memories[2].id).toBe("target"); }); it("should handle bidirectional waypoint links", () => { // Setup: A <-> B bidirectional link // Memories are created to establish context createMockMemory("memory-A", "Memory A"); createMockMemory("memory-B", "Memory B"); // Bidirectional links const linkAB = createMockLink("memory-A", "memory-B", 0.8); const linkBA = createMockLink("memory-B", "memory-A", 0.8); // Verify bidirectional structure expect(linkAB.sourceId).toBe("memory-A"); expect(linkAB.targetId).toBe("memory-B"); expect(linkBA.sourceId).toBe("memory-B"); expect(linkBA.targetId).toBe("memory-A"); expect(linkAB.weight).toBe(linkBA.weight); }); }); describe("Waypoint Link Weight Filtering", () => { it("should filter traversal by minimum link weight", () => { // Setup: A -> B (weight 0.9), A -> C (weight 0.3) // Memories are created to establish context createMockMemory("memory-A", "Memory A"); createMockMemory("memory-B", "Memory B"); createMockMemory("memory-C", "Memory C"); const links = [ createMockLink("memory-A", "memory-B", 0.9), createMockLink("memory-A", "memory-C", 0.3), ]; // Filter by minWeight 0.5 const minWeight = 0.5; const filteredLinks = links.filter((link) => link.weight >= minWeight); expect(filteredLinks.length).toBe(1); expect(filteredLinks[0].targetId).toBe("memory-B"); expect(filteredLinks[0].weight).toBe(0.9); }); it("should include all links when minWeight is 0", () => { const links = [ createMockLink("memory-A", "memory-B", 0.9), createMockLink("memory-A", "memory-C", 0.1), createMockLink("memory-A", "memory-D", 0.0), ]; // With minWeight 0, all links should be included const minWeight = 0; const filteredLinks = links.filter((link) => link.weight >= minWeight); expect(filteredLinks.length).toBe(3); }); it("should exclude all links when minWeight is 1", () => { const links = [ createMockLink("memory-A", "memory-B", 0.9), createMockLink("memory-A", "memory-C", 0.5), ]; const minWeight = 1.0; const filteredLinks = links.filter((link) => link.weight >= minWeight); expect(filteredLinks.length).toBe(0); }); }); describe("Link Type Classification in Traversal", () => { it("should traverse links of different types", () => { // Setup: A -> B (semantic), B -> C (causal), C -> D (temporal) const links = [ createMockLink("memory-A", "memory-B", 0.8, LinkType.Semantic), createMockLink("memory-B", "memory-C", 0.7, LinkType.Causal), createMockLink("memory-C", "memory-D", 0.6, LinkType.Temporal), ]; expect(links[0].linkType).toBe(LinkType.Semantic); expect(links[1].linkType).toBe(LinkType.Causal); expect(links[2].linkType).toBe(LinkType.Temporal); }); it("should support analogical link type", () => { const link = createMockLink("parent", "child", 0.9, LinkType.Analogical); expect(link.linkType).toBe(LinkType.Analogical); }); }); describe("Cyclic Graph Handling", () => { it("should handle cycles without infinite loops", () => { // Setup: A -> B -> C -> A (cycle) const memoryA = createMockMemory("memory-A", "Memory A"); const memoryB = createMockMemory("memory-B", "Memory B"); const memoryC = createMockMemory("memory-C", "Memory C"); const links = [ createMockLink("memory-A", "memory-B", 0.8), createMockLink("memory-B", "memory-C", 0.7), createMockLink("memory-C", "memory-A", 0.9), // Cycle back to A ]; // Simulate BFS with visited set const visited = new Set<string>(); const memories = [memoryA, memoryB, memoryC]; const result: Memory[] = []; const queue: Memory[] = [memoryA]; visited.add(memoryA.id); while (queue.length > 0) { const current = queue.shift()!; result.push(current); // Get outgoing links for current memory const outgoingLinks = links.filter((l) => l.sourceId === current.id); for (const link of outgoingLinks) { if (!visited.has(link.targetId)) { visited.add(link.targetId); const targetMemory = memories.find((m) => m.id === link.targetId); if (targetMemory) { queue.push(targetMemory); } } } } // Should visit each node exactly once despite cycle expect(result.length).toBe(3); expect(visited.size).toBe(3); }); }); describe("Waypoint Expansion", () => { it("should expand from waypoint for specified number of hops", () => { // Setup: A -> B -> C -> D (4 nodes, 3 hops) const memories = [ createMockMemory("waypoint-A", "Waypoint A"), createMockMemory("waypoint-B", "Waypoint B"), createMockMemory("waypoint-C", "Waypoint C"), createMockMemory("waypoint-D", "Waypoint D"), ]; const links = [ createMockLink("waypoint-A", "waypoint-B", 0.8), createMockLink("waypoint-B", "waypoint-C", 0.7), createMockLink("waypoint-C", "waypoint-D", 0.6), ]; // Simulate expandViaWaypoint with 2 hops const maxHops = 2; const visited = new Set<string>(); const result: Memory[] = []; const queue: Array<{ memory: Memory; depth: number }> = []; queue.push({ memory: memories[0], depth: 0 }); visited.add(memories[0].id); while (queue.length > 0) { const current = queue.shift()!; result.push(current.memory); if (current.depth >= maxHops) continue; const outgoingLinks = links.filter((l) => l.sourceId === current.memory.id); for (const link of outgoingLinks) { if (!visited.has(link.targetId)) { visited.add(link.targetId); const targetMemory = memories.find((m) => m.id === link.targetId); if (targetMemory) { queue.push({ memory: targetMemory, depth: current.depth + 1 }); } } } } // With maxHops = 2, we should have A, B, C (3 memories) expect(result.length).toBe(3); expect(result.map((m) => m.id)).toContain("waypoint-A"); expect(result.map((m) => m.id)).toContain("waypoint-B"); expect(result.map((m) => m.id)).toContain("waypoint-C"); expect(result.map((m) => m.id)).not.toContain("waypoint-D"); }); it("should return only start node for 0 hops", () => { const startMemory = createMockMemory("waypoint-start", "Start waypoint"); // With 0 hops, only return start node (maxHops = 0) const result: Memory[] = [startMemory]; expect(result.length).toBe(1); expect(result[0].id).toBe("waypoint-start"); }); it("should return empty array for negative hops", () => { const maxHops = -1; const result: Memory[] = maxHops < 0 ? [] : [createMockMemory("start", "Start")]; expect(result.length).toBe(0); }); }); describe("Path Explanation Generation", () => { it("should generate human-readable path explanation", () => { const path: Path = { memories: [ createMockMemory("A", "First concept about TypeScript"), createMockMemory("B", "Related concept about JavaScript"), createMockMemory("C", "Final concept about programming"), ], links: [ createMockLink("A", "B", 0.85, LinkType.Semantic), createMockLink("B", "C", 0.72, LinkType.Causal), ], totalWeight: 0.785, explanation: "", }; // Simulate explainPath logic const parts: string[] = []; for (let i = 0; i < path.memories.length; i++) { const memory = path.memories[i]; const truncatedContent = memory.content.length > 50 ? `${memory.content.substring(0, 47)}...` : memory.content; parts.push(truncatedContent); if (i < path.links.length) { const link = path.links[i]; parts.push(` → [${link.linkType}, ${link.weight.toFixed(2)}] → `); } } parts.push(` (total strength: ${path.totalWeight.toFixed(2)})`); const explanation = parts.join(""); expect(explanation).toContain("First concept about TypeScript"); expect(explanation).toContain("Related concept about JavaScript"); expect(explanation).toContain("Final concept about programming"); expect(explanation).toContain("semantic"); expect(explanation).toContain("causal"); expect(explanation).toContain("0.85"); expect(explanation).toContain("0.72"); }); it("should handle empty path", () => { const emptyPath: Path = { memories: [], links: [], totalWeight: 0, explanation: "", }; const explanation = emptyPath.memories.length === 0 ? "No path found" : ""; expect(explanation).toBe("No path found"); }); it("should handle single memory path", () => { const singlePath: Path = { memories: [createMockMemory("single", "Single memory content")], links: [], totalWeight: 0, explanation: "", }; const explanation = singlePath.memories.length === 1 ? singlePath.memories[0].content : ""; expect(explanation).toBe("Single memory content"); }); it("should truncate long content in explanation", () => { const longContent = "A".repeat(100); const path: Path = { memories: [ createMockMemory("long", longContent), createMockMemory("short", "Short content"), ], links: [createMockLink("long", "short", 0.8, LinkType.Semantic)], totalWeight: 0.8, explanation: "", }; const truncatedContent = path.memories[0].content.length > 50 ? `${path.memories[0].content.substring(0, 47)}...` : path.memories[0].content; expect(truncatedContent).toContain("..."); expect(truncatedContent.length).toBeLessThanOrEqual(50); }); }); describe("Path Weight Calculation", () => { it("should calculate average weight for path", () => { const links = [createMockLink("A", "B", 0.8), createMockLink("B", "C", 0.6)]; // Calculate average weight const totalWeight = links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0; expect(totalWeight).toBeCloseTo(0.7, 2); }); it("should return 0 for empty path", () => { const links: Link[] = []; const totalWeight = links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0; expect(totalWeight).toBe(0); }); it("should handle single link path", () => { const links = [createMockLink("A", "B", 0.9)]; const totalWeight = links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0; expect(totalWeight).toBe(0.9); }); }); describe("Embedding-Based Link Weight Calculation", () => { it("should use embedding similarity for link weight calculation", () => { // Simulate embedding similarity calculation const embeddingA = new Array(768).fill(0).map((_, i) => Math.sin(i) * 0.1); const embeddingB = new Array(768).fill(0).map((_, i) => Math.sin(i + 0.1) * 0.1); // Calculate cosine similarity let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < embeddingA.length; i++) { dotProduct += embeddingA[i] * embeddingB[i]; normA += embeddingA[i] * embeddingA[i]; normB += embeddingB[i] * embeddingB[i]; } const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); // Similar embeddings should have high similarity expect(similarity).toBeGreaterThan(0.9); }); it("should handle orthogonal embeddings", () => { // Create orthogonal embeddings const embeddingA = new Array(768).fill(0); const embeddingB = new Array(768).fill(0); embeddingA[0] = 1; embeddingB[1] = 1; let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < embeddingA.length; i++) { dotProduct += embeddingA[i] * embeddingB[i]; normA += embeddingA[i] * embeddingA[i]; normB += embeddingB[i] * embeddingB[i]; } const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); // Orthogonal embeddings should have 0 similarity expect(similarity).toBe(0); }); }); describe("Metadata-Based Link Classification", () => { it("should classify links based on memory metadata", () => { // Parent-child relationship -> Analogical const parentMemory = createMockMemory("parent", "Parent project", { metadata: { keywords: ["project"], tags: [], category: "project", context: "", importance: 0.8, isAtomic: false, }, }); const childMemory = createMockMemory("child", "Child task", { metadata: { keywords: ["task"], tags: [], category: "task", context: "", importance: 0.6, isAtomic: true, parentId: "parent", }, }); // Verify parent-child relationship expect(childMemory.metadata.parentId).toBe("parent"); expect(parentMemory.metadata.category).toBe("project"); expect(childMemory.metadata.category).toBe("task"); }); it("should detect causal relationships from keywords", () => { const causeMemory = createMockMemory("cause", "User clicked button", { metadata: { keywords: ["action", "click", "trigger"], tags: [], category: "event", context: "", importance: 0.7, isAtomic: true, }, }); const effectMemory = createMockMemory("effect", "Form was submitted", { metadata: { keywords: ["result", "submit", "effect"], tags: [], category: "event", context: "", importance: 0.7, isAtomic: true, }, }); const causeKeywords = ["action", "click", "trigger"]; const effectKeywords = ["result", "submit", "effect"]; expect(causeMemory.metadata.keywords.some((k) => causeKeywords.includes(k))).toBe(true); expect(effectMemory.metadata.keywords.some((k) => effectKeywords.includes(k))).toBe(true); }); it("should detect temporal relationships from session and timestamp", () => { const now = new Date(); const twoMinutesLater = new Date(now.getTime() + 2 * 60 * 1000); const memory1 = createMockMemory("mem1", "First event", { sessionId: "session-123", createdAt: now, }); const memory2 = createMockMemory("mem2", "Second event", { sessionId: "session-123", createdAt: twoMinutesLater, }); // Same session expect(memory1.sessionId).toBe(memory2.sessionId); // Close in time (within 5 minutes) const timeDiff = Math.abs(memory2.createdAt.getTime() - memory1.createdAt.getTime()) / 60000; expect(timeDiff).toBeLessThanOrEqual(5); }); }); });

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/keyurgolani/ThoughtMcp'

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