Skip to main content
Glama
waypoint-link-integration.test.ts25.7 kB
/** * Waypoint Link Integration Tests * * Tests for waypoint link creation, storage, and deletion in the context * of memory repository operations. * * **Feature: mcp-tool-improvements, Task 5.6: Write unit tests for waypoint link creation** * * Tests cover: * - Link creation for similar memories * - Bidirectional links * - Link removal on delete * * **Validates: Requirements 14.2, 14.3, 14.4, 14.5** */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { Link, LinkType } from "../../../graph/types"; import { MemoryRepository } from "../../../memory/memory-repository"; import type { MemoryContent } from "../../../memory/types"; import { createTestSectorEmbeddings } from "../../utils/test-fixtures"; describe("Waypoint Link Integration - Link Creation for Similar Memories", () => { /** * **Validates: Requirements 14.1, 14.2, 14.3** * * Tests that when a memory is stored with similar memories, * the system creates waypoint graph links. */ let repository: MemoryRepository; let mockDb: any; let mockEmbeddingEngine: any; let mockGraphBuilder: any; let mockEmbeddingStorage: any; let mockClient: any; beforeEach(() => { // Mock database client with query method mockClient = { query: vi.fn().mockImplementation((query: string) => { // Return candidate memories for waypoint connection creation if (query.includes("SELECT m.*, me.embedding")) { return Promise.resolve({ rows: [ { id: "existing-memory-1", content: "Similar memory content about machine learning", created_at: new Date(), last_accessed: new Date(), access_count: 0, salience: 0.7, decay_rate: 0.02, strength: 1.0, user_id: "user-123", session_id: "session-456", primary_sector: "semantic", }, { id: "existing-memory-2", content: "Another similar memory about AI", created_at: new Date(), last_accessed: new Date(), access_count: 0, salience: 0.6, decay_rate: 0.02, strength: 1.0, user_id: "user-123", session_id: "session-456", primary_sector: "semantic", }, ], }); } return Promise.resolve({ rows: [] }); }), }; // Mock database connection manager mockDb = { beginTransaction: vi.fn().mockResolvedValue(mockClient), commitTransaction: vi.fn().mockResolvedValue(undefined), rollbackTransaction: vi.fn().mockResolvedValue(undefined), getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), query: vi.fn().mockResolvedValue({ rows: [] }), }; // Mock embedding engine mockEmbeddingEngine = { generateAllSectorEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), generateSemanticEmbedding: vi.fn().mockResolvedValue(new Array(1536).fill(0.1)), }; // Mock waypoint graph builder - returns links for similar memories mockGraphBuilder = { createWaypointLinks: vi.fn().mockResolvedValue({ links: [ { sourceId: "new-memory-id", targetId: "existing-memory-1", linkType: LinkType.Semantic, weight: 0.85, createdAt: new Date(), traversalCount: 0, }, { sourceId: "existing-memory-1", targetId: "new-memory-id", linkType: LinkType.Semantic, weight: 0.85, createdAt: new Date(), traversalCount: 0, }, ], skippedCount: 1, }), }; // Mock embedding storage mockEmbeddingStorage = { storeEmbeddings: vi.fn().mockResolvedValue(undefined), retrieveEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), vectorSimilaritySearch: vi.fn().mockResolvedValue([]), }; repository = new MemoryRepository( mockDb, mockEmbeddingEngine, mockGraphBuilder, mockEmbeddingStorage ); }); afterEach(() => { vi.clearAllMocks(); }); it("should create links when similar memories exist", async () => { /** * Requirement 14.1: WHEN a memory is stored THEN the system SHALL attempt * to create waypoint graph links to related memories */ const content: MemoryContent = { content: "Machine learning algorithms for data analysis", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); // Verify graph builder was called to create links expect(mockGraphBuilder.createWaypointLinks).toHaveBeenCalled(); // Verify links were created expect(memory.links).toBeDefined(); expect(memory.links!.length).toBeGreaterThan(0); }); it("should create links with weight above similarity threshold", async () => { /** * Requirement 14.2: WHEN similar memories exist (similarity > 0.7) THEN * the system SHALL create bidirectional links between them */ const content: MemoryContent = { content: "Deep learning neural networks", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); // All created links should have weight >= 0.7 (similarity threshold) if (memory.links && memory.links.length > 0) { memory.links.forEach((link: Link) => { expect(link.weight).toBeGreaterThanOrEqual(0.7); }); } }); it("should return accurate linksCreated count", async () => { /** * Requirement 14.3: WHEN links are created THEN the linksCreated count * in the response SHALL reflect the actual number of links created */ const content: MemoryContent = { content: "Test memory for link count verification", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); // The links array length should match the actual links created expect(memory.links).toBeDefined(); expect(Array.isArray(memory.links)).toBe(true); expect(memory.links!.length).toBe(2); // Based on mock returning 2 links }); it("should handle case with no similar memories", async () => { /** * Requirement 14.1: System should handle case when no similar memories exist */ // Mock graph builder to return no links mockGraphBuilder.createWaypointLinks.mockResolvedValue({ links: [], skippedCount: 0, }); const content: MemoryContent = { content: "Completely unique content with no matches", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); expect(memory.links).toBeDefined(); expect(memory.links!.length).toBe(0); }); it("should store links in database", async () => { /** * Requirement 14.1: Links should be persisted to database */ const content: MemoryContent = { content: "Memory with links to store", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; await repository.create(content); // Verify INSERT INTO memory_links was called const insertCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("INSERT INTO memory_links") ); expect(insertCalls.length).toBeGreaterThan(0); }); }); describe("Waypoint Link Integration - Bidirectional Links", () => { /** * **Validates: Requirements 14.2** * * Tests that bidirectional links are created correctly. */ let repository: MemoryRepository; let mockDb: any; let mockEmbeddingEngine: any; let mockGraphBuilder: any; let mockEmbeddingStorage: any; let mockClient: any; beforeEach(() => { mockClient = { query: vi.fn().mockImplementation((query: string) => { if (query.includes("SELECT m.*, me.embedding")) { return Promise.resolve({ rows: [ { id: "existing-memory-1", content: "Existing memory content", created_at: new Date(), last_accessed: new Date(), access_count: 0, salience: 0.5, decay_rate: 0.02, strength: 1.0, user_id: "user-123", session_id: "session-456", primary_sector: "semantic", }, ], }); } return Promise.resolve({ rows: [] }); }), }; mockDb = { beginTransaction: vi.fn().mockResolvedValue(mockClient), commitTransaction: vi.fn().mockResolvedValue(undefined), rollbackTransaction: vi.fn().mockResolvedValue(undefined), getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), query: vi.fn().mockResolvedValue({ rows: [] }), }; mockEmbeddingEngine = { generateAllSectorEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), generateSemanticEmbedding: vi.fn().mockResolvedValue(new Array(1536).fill(0.1)), }; // Mock graph builder to return bidirectional links mockGraphBuilder = { createWaypointLinks: vi.fn().mockResolvedValue({ links: [ { sourceId: "new-memory-id", targetId: "existing-memory-1", linkType: LinkType.Semantic, weight: 0.85, createdAt: new Date(), traversalCount: 0, }, { sourceId: "existing-memory-1", targetId: "new-memory-id", linkType: LinkType.Semantic, weight: 0.85, createdAt: new Date(), traversalCount: 0, }, ], skippedCount: 0, }), }; mockEmbeddingStorage = { storeEmbeddings: vi.fn().mockResolvedValue(undefined), retrieveEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), vectorSimilaritySearch: vi.fn().mockResolvedValue([]), }; repository = new MemoryRepository( mockDb, mockEmbeddingEngine, mockGraphBuilder, mockEmbeddingStorage ); }); afterEach(() => { vi.clearAllMocks(); }); it("should create bidirectional links between memories", async () => { /** * Requirement 14.2: WHEN similar memories exist (similarity > 0.7) THEN * the system SHALL create bidirectional links between them */ const content: MemoryContent = { content: "Test memory for bidirectional links", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); expect(memory.links).toBeDefined(); expect(memory.links!.length).toBe(2); // Verify bidirectional: one link from new to existing, one from existing to new const forwardLink = memory.links!.find( (l: Link) => l.sourceId === "new-memory-id" && l.targetId === "existing-memory-1" ); const reverseLink = memory.links!.find( (l: Link) => l.sourceId === "existing-memory-1" && l.targetId === "new-memory-id" ); expect(forwardLink).toBeDefined(); expect(reverseLink).toBeDefined(); }); it("should have symmetric weights for bidirectional links", async () => { /** * Requirement 14.2: Bidirectional links should have symmetric weights */ const content: MemoryContent = { content: "Test memory for symmetric weights", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); if (memory.links && memory.links.length >= 2) { const forwardLink = memory.links.find( (l: Link) => l.sourceId === "new-memory-id" && l.targetId === "existing-memory-1" ); const reverseLink = memory.links.find( (l: Link) => l.sourceId === "existing-memory-1" && l.targetId === "new-memory-id" ); if (forwardLink && reverseLink) { expect(forwardLink.weight).toBe(reverseLink.weight); } } }); it("should have same link type for bidirectional links", async () => { /** * Requirement 14.2: Bidirectional links should have same link type */ const content: MemoryContent = { content: "Test memory for link type consistency", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; const memory = await repository.create(content); if (memory.links && memory.links.length >= 2) { const forwardLink = memory.links.find( (l: Link) => l.sourceId === "new-memory-id" && l.targetId === "existing-memory-1" ); const reverseLink = memory.links.find( (l: Link) => l.sourceId === "existing-memory-1" && l.targetId === "new-memory-id" ); if (forwardLink && reverseLink) { expect(forwardLink.linkType).toBe(reverseLink.linkType); } } }); it("should store both directions of bidirectional links in database", async () => { /** * Requirement 14.2: Both link directions should be persisted */ const content: MemoryContent = { content: "Test memory for database storage", userId: "user-123", sessionId: "session-456", primarySector: "semantic", }; await repository.create(content); // Count INSERT INTO memory_links calls const insertCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("INSERT INTO memory_links") ); // Should have 2 insert calls for bidirectional links expect(insertCalls.length).toBe(2); }); }); describe("Waypoint Link Integration - Link Removal on Delete", () => { /** * **Validates: Requirements 14.5** * * Tests that when a memory is deleted, associated waypoint graph links * are also removed. */ let repository: MemoryRepository; let mockDb: any; let mockEmbeddingEngine: any; let mockGraphBuilder: any; let mockEmbeddingStorage: any; let mockClient: any; let deletedLinks: string[] = []; beforeEach(() => { deletedLinks = []; mockClient = { query: vi.fn().mockImplementation((query: string, params?: any[]) => { // Track link deletions if (query.includes("DELETE FROM memory_links")) { if (params && params[0]) { deletedLinks.push(`source:${params[0]}`); } return Promise.resolve({ rows: [], rowCount: 1 }); } // Return memory for delete verification if (query.includes("DELETE FROM memories") && query.includes("RETURNING")) { return Promise.resolve({ rows: [{ id: params?.[0] || "test-memory-id" }], rowCount: 1, }); } return Promise.resolve({ rows: [] }); }), }; mockDb = { beginTransaction: vi.fn().mockResolvedValue(mockClient), commitTransaction: vi.fn().mockResolvedValue(undefined), rollbackTransaction: vi.fn().mockResolvedValue(undefined), getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), query: vi.fn().mockResolvedValue({ rows: [] }), }; mockEmbeddingEngine = { generateAllSectorEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), generateSemanticEmbedding: vi.fn().mockResolvedValue(new Array(1536).fill(0.1)), }; mockGraphBuilder = { createWaypointLinks: vi.fn().mockResolvedValue({ links: [], skippedCount: 0, }), }; mockEmbeddingStorage = { storeEmbeddings: vi.fn().mockResolvedValue(undefined), retrieveEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), vectorSimilaritySearch: vi.fn().mockResolvedValue([]), }; repository = new MemoryRepository( mockDb, mockEmbeddingEngine, mockGraphBuilder, mockEmbeddingStorage ); }); afterEach(() => { vi.clearAllMocks(); }); it("should delete links where memory is source on hard delete", async () => { /** * Requirement 14.5: WHEN a memory is deleted THEN the associated * waypoint graph links SHALL also be removed */ const memoryId = "test-memory-to-delete"; await repository.delete(memoryId, false); // hard delete // Verify DELETE FROM memory_links WHERE source_id was called const sourceDeleteCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("DELETE FROM memory_links") && call[0].includes("source_id") ); expect(sourceDeleteCalls.length).toBeGreaterThan(0); }); it("should delete links where memory is target on hard delete", async () => { /** * Requirement 14.5: Links where memory is target should also be removed */ const memoryId = "test-memory-to-delete"; await repository.delete(memoryId, false); // hard delete // Verify DELETE FROM memory_links WHERE target_id was called const targetDeleteCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("DELETE FROM memory_links") && call[0].includes("target_id") ); expect(targetDeleteCalls.length).toBeGreaterThan(0); }); it("should not delete links on soft delete", async () => { /** * Requirement 14.5: Soft delete should preserve links */ // Reset mock to track calls mockClient.query.mockClear(); // Mock soft delete to return the memory mockClient.query.mockImplementation((query: string, params?: any[]) => { if (query.includes("UPDATE memories") && query.includes("strength = 0")) { return Promise.resolve({ rows: [{ id: params?.[0] || "test-memory-id" }], rowCount: 1, }); } return Promise.resolve({ rows: [] }); }); const memoryId = "test-memory-soft-delete"; await repository.delete(memoryId, true); // soft delete // Verify no DELETE FROM memory_links was called const linkDeleteCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("DELETE FROM memory_links") ); expect(linkDeleteCalls.length).toBe(0); }); it("should delete both source and target links for bidirectional connections", async () => { /** * Requirement 14.5: Both directions of bidirectional links should be removed */ const memoryId = "test-memory-bidirectional"; await repository.delete(memoryId, false); // hard delete // Should have calls for both source_id and target_id deletions const allDeleteCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("DELETE FROM memory_links") ); // Should have at least 2 delete calls (one for source, one for target) expect(allDeleteCalls.length).toBeGreaterThanOrEqual(2); }); it("should delete links before deleting memory record", async () => { /** * Requirement 14.5: Links should be deleted before memory to maintain referential integrity */ const callOrder: string[] = []; mockClient.query.mockImplementation((query: string, params?: any[]) => { if (query.includes("DELETE FROM memory_links")) { callOrder.push("delete_links"); } if (query.includes("DELETE FROM memories")) { callOrder.push("delete_memory"); return Promise.resolve({ rows: [{ id: params?.[0] || "test-memory-id" }], rowCount: 1, }); } return Promise.resolve({ rows: [] }); }); const memoryId = "test-memory-order"; await repository.delete(memoryId, false); // hard delete // Links should be deleted before memory const linkDeleteIndex = callOrder.indexOf("delete_links"); const memoryDeleteIndex = callOrder.indexOf("delete_memory"); expect(linkDeleteIndex).toBeLessThan(memoryDeleteIndex); }); it("should handle delete when memory has no links", async () => { /** * Requirement 14.5: Delete should work even if memory has no links */ mockClient.query.mockImplementation((query: string, params?: any[]) => { if (query.includes("DELETE FROM memory_links")) { return Promise.resolve({ rows: [], rowCount: 0 }); // No links deleted } if (query.includes("DELETE FROM memories") && query.includes("RETURNING")) { return Promise.resolve({ rows: [{ id: params?.[0] || "test-memory-id" }], rowCount: 1, }); } return Promise.resolve({ rows: [] }); }); const memoryId = "test-memory-no-links"; // Should not throw await expect(repository.delete(memoryId, false)).resolves.not.toThrow(); }); }); describe("Waypoint Link Integration - Link Weight in Composite Scoring", () => { /** * **Validates: Requirements 14.4** * * Tests that linkWeight reflects actual connections in retrieve_memories. */ let repository: MemoryRepository; let mockDb: any; let mockEmbeddingEngine: any; let mockGraphBuilder: any; let mockEmbeddingStorage: any; let mockClient: any; beforeEach(() => { mockClient = { query: vi.fn().mockImplementation((query: string, _params?: any[]) => { // Return link weights for composite scoring if (query.includes("SELECT") && query.includes("memory_links") && query.includes("AVG")) { return Promise.resolve({ rows: [ { memory_id: "memory-1", avg_weight: 0.85 }, { memory_id: "memory-2", avg_weight: 0.75 }, ], }); } // Return memories for search if (query.includes("SELECT") && query.includes("FROM memories")) { return Promise.resolve({ rows: [ { id: "memory-1", content: "Test memory 1", created_at: new Date(), last_accessed: new Date(), access_count: 0, salience: 0.5, decay_rate: 0.02, strength: 1.0, user_id: "user-123", session_id: "session-456", primary_sector: "semantic", keywords: [], tags: [], category: "general", context: "", importance: 0.5, is_atomic: true, }, ], }); } return Promise.resolve({ rows: [] }); }), }; mockDb = { beginTransaction: vi.fn().mockResolvedValue(mockClient), commitTransaction: vi.fn().mockResolvedValue(undefined), rollbackTransaction: vi.fn().mockResolvedValue(undefined), getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), query: vi.fn().mockResolvedValue({ rows: [] }), }; mockEmbeddingEngine = { generateAllSectorEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), generateSemanticEmbedding: vi.fn().mockResolvedValue(new Array(1536).fill(0.1)), }; mockGraphBuilder = { createWaypointLinks: vi.fn().mockResolvedValue({ links: [], skippedCount: 0, }), }; mockEmbeddingStorage = { storeEmbeddings: vi.fn().mockResolvedValue(undefined), retrieveEmbeddings: vi.fn().mockResolvedValue(createTestSectorEmbeddings()), vectorSimilaritySearch: vi .fn() .mockResolvedValue([{ memoryId: "memory-1", similarity: 0.9 }]), }; repository = new MemoryRepository( mockDb, mockEmbeddingEngine, mockGraphBuilder, mockEmbeddingStorage ); }); afterEach(() => { vi.clearAllMocks(); }); it("should query link weights for composite scoring", async () => { /** * Requirement 14.4: WHEN retrieving memories THEN the linkWeight component * of composite scoring SHALL reflect actual link connections */ await repository.search({ userId: "user-123", text: "test query", limit: 10, }); // Verify link weight query was made const linkWeightCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("memory_links") && call[0].includes("AVG") ); expect(linkWeightCalls.length).toBeGreaterThan(0); }); it("should include both source and target links in weight calculation", async () => { /** * Requirement 14.4: Link weight should consider both directions */ await repository.search({ userId: "user-123", text: "test query", limit: 10, }); // Verify query includes both source_id and target_id const linkWeightCalls = mockClient.query.mock.calls.filter( (call: any[]) => typeof call[0] === "string" && call[0].includes("memory_links") ); const hasSourceAndTarget = linkWeightCalls.some( (call: any[]) => call[0].includes("source_id") && call[0].includes("target_id") ); expect(hasSourceAndTarget).toBe(true); }); });

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