Skip to main content
Glama
memory-lifecycle.test.ts39 kB
/** * Memory Lifecycle Integration Tests (Mocked) * * Tests the interaction between MemoryRepository, EmbeddingEngine, and WaypointBuilder * using mocks for external dependencies (database, embedding model). * * 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 type { PoolClient } from "pg"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Memory, MemoryContent, MemoryMetadata } from "../../memory/types"; // Create mock database client function createMockClient(): PoolClient { const mockClient = { query: vi.fn(), release: vi.fn(), } as unknown as PoolClient; return mockClient; } // Create mock database manager function createMockDbManager() { const mockClient = createMockClient(); return { client: mockClient, manager: { getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), beginTransaction: vi.fn().mockResolvedValue(mockClient), commitTransaction: vi.fn().mockResolvedValue(undefined), rollbackTransaction: vi.fn().mockResolvedValue(undefined), connect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined), }, }; } // Create mock embedding engine function createMockEmbeddingEngine() { const mockEmbedding = new Array(1536).fill(0).map((_, i) => Math.sin(i) * 0.1); return { generateAllSectorEmbeddings: vi.fn().mockResolvedValue({ episodic: mockEmbedding, semantic: mockEmbedding, procedural: mockEmbedding, emotional: mockEmbedding, reflective: mockEmbedding, }), generateSemanticEmbedding: vi.fn().mockResolvedValue(mockEmbedding), generateEpisodicEmbedding: vi.fn().mockResolvedValue(mockEmbedding), generateProceduralEmbedding: vi.fn().mockResolvedValue(mockEmbedding), generateEmotionalEmbedding: vi.fn().mockResolvedValue(mockEmbedding), generateReflectiveEmbedding: vi.fn().mockResolvedValue(mockEmbedding), }; } // Create mock embedding storage function createMockEmbeddingStorage() { return { storeEmbeddings: vi.fn().mockResolvedValue(undefined), retrieveEmbeddings: vi.fn().mockResolvedValue({ episodic: new Array(1536).fill(0.1), semantic: new Array(1536).fill(0.1), procedural: new Array(1536).fill(0.1), emotional: new Array(1536).fill(0.1), reflective: new Array(1536).fill(0.1), }), vectorSimilaritySearch: vi.fn().mockResolvedValue([]), deleteEmbeddings: vi.fn().mockResolvedValue(undefined), }; } // Create mock waypoint graph builder function createMockGraphBuilder() { return { createWaypointLinks: vi.fn().mockResolvedValue({ links: [], processingTime: 10, }), deleteLinksForMemory: vi.fn().mockResolvedValue(undefined), }; } describe("Memory Lifecycle Integration (Mocked)", () => { let mockDb: ReturnType<typeof createMockDbManager>; let mockEmbeddingEngine: ReturnType<typeof createMockEmbeddingEngine>; let mockEmbeddingStorage: ReturnType<typeof createMockEmbeddingStorage>; let mockGraphBuilder: ReturnType<typeof createMockGraphBuilder>; beforeEach(() => { vi.clearAllMocks(); mockDb = createMockDbManager(); mockEmbeddingEngine = createMockEmbeddingEngine(); mockEmbeddingStorage = createMockEmbeddingStorage(); mockGraphBuilder = createMockGraphBuilder(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Memory Creation Flow", () => { it("should coordinate memory creation across repository, embedding engine, and graph builder", async () => { // Setup mock responses for the creation flow const mockMemoryId = "test-memory-123"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Mock the database insert to succeed (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 1, }); // Simulate the memory creation flow const content: MemoryContent = { content: "Test memory content for integration", userId: "test-user-1", sessionId: "test-session-1", primarySector: "semantic", }; const metadata: MemoryMetadata = { keywords: ["test", "integration"], tags: ["unit-test"], category: "testing", importance: 0.8, }; // Step 1: Begin transaction const client = await mockDb.manager.beginTransaction(); expect(mockDb.manager.beginTransaction).toHaveBeenCalled(); // Step 2: Generate embeddings const embeddings = await mockEmbeddingEngine.generateAllSectorEmbeddings({ text: content.content, sector: content.primarySector, }); expect(mockEmbeddingEngine.generateAllSectorEmbeddings).toHaveBeenCalledWith({ text: content.content, sector: content.primarySector, }); expect(embeddings).toHaveProperty("semantic"); expect(embeddings).toHaveProperty("episodic"); // Step 3: Store embeddings await mockEmbeddingStorage.storeEmbeddings(mockMemoryId, embeddings, "default", client); expect(mockEmbeddingStorage.storeEmbeddings).toHaveBeenCalledWith( mockMemoryId, embeddings, "default", client ); // Step 4: Create waypoint links const mockMemory: Memory = { id: mockMemoryId, content: content.content, createdAt: mockTimestamp, lastAccessed: mockTimestamp, accessCount: 0, salience: 0.5, decayRate: 0.01, strength: 1.0, userId: content.userId, sessionId: content.sessionId, primarySector: content.primarySector, metadata: metadata, embeddings: embeddings, }; const linkResult = await mockGraphBuilder.createWaypointLinks(mockMemory, []); expect(mockGraphBuilder.createWaypointLinks).toHaveBeenCalled(); expect(linkResult).toHaveProperty("links"); // Step 5: Commit transaction await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalledWith(client); }); it("should rollback transaction on embedding generation failure", async () => { // Setup mock to fail on embedding generation mockEmbeddingEngine.generateAllSectorEmbeddings.mockRejectedValue( new Error("Embedding generation failed") ); const content: MemoryContent = { content: "Test memory content", userId: "test-user-1", sessionId: "test-session-1", primarySector: "semantic", }; // Begin transaction const client = await mockDb.manager.beginTransaction(); // Attempt embedding generation (should fail) await expect( mockEmbeddingEngine.generateAllSectorEmbeddings({ text: content.content, sector: content.primarySector, }) ).rejects.toThrow("Embedding generation failed"); // Rollback should be called on failure await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalledWith(client); }); it("should handle waypoint link creation failure gracefully", async () => { // Setup mock to fail on waypoint creation mockGraphBuilder.createWaypointLinks.mockRejectedValue(new Error("Waypoint creation failed")); const mockMemory: Memory = { id: "test-memory-456", content: "Test content", createdAt: new Date(), lastAccessed: new Date(), accessCount: 0, salience: 0.5, decayRate: 0.01, strength: 1.0, userId: "test-user", sessionId: "test-session", primarySector: "semantic", metadata: { keywords: [], tags: [] }, }; // Waypoint creation failure should be caught await expect(mockGraphBuilder.createWaypointLinks(mockMemory, [])).rejects.toThrow( "Waypoint creation failed" ); // In real implementation, this would be caught and logged, not propagated }); }); describe("Memory Retrieval Flow", () => { it("should coordinate retrieval across repository and embedding storage", async () => { const mockMemoryId = "test-memory-789"; const mockUserId = "test-user-1"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Setup mock database response (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [ { id: mockMemoryId, content: "Retrieved memory content", created_at: mockTimestamp.toISOString(), last_accessed: mockTimestamp.toISOString(), access_count: 5, salience: 0.7, decay_rate: 0.01, strength: 0.9, user_id: mockUserId, session_id: "test-session", primary_sector: "semantic", keywords: ["test"], tags: ["integration"], category: "testing", context: "test context", importance: 0.8, is_atomic: true, parent_id: null, }, ], rowCount: 1, }) .mockResolvedValueOnce({ rows: [], // No links rowCount: 0, }); // Step 1: Get connection const client = await mockDb.manager.getConnection(); expect(mockDb.manager.getConnection).toHaveBeenCalled(); // Step 2: Query memory const memoryResult = await client.query( "SELECT * FROM memories WHERE id = $1 AND user_id = $2", [mockMemoryId, mockUserId] ); expect(memoryResult.rows.length).toBe(1); expect(memoryResult.rows[0].id).toBe(mockMemoryId); // Step 3: Retrieve embeddings const embeddings = await mockEmbeddingStorage.retrieveEmbeddings(mockMemoryId); expect(mockEmbeddingStorage.retrieveEmbeddings).toHaveBeenCalledWith(mockMemoryId); expect(embeddings).toHaveProperty("semantic"); // Step 4: Release connection mockDb.manager.releaseConnection(client); expect(mockDb.manager.releaseConnection).toHaveBeenCalledWith(client); }); it("should return null for non-existent memory", async () => { const mockMemoryId = "non-existent-memory"; const mockUserId = "test-user-1"; // Setup mock to return empty result (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 0, }); const client = await mockDb.manager.getConnection(); const result = await client.query("SELECT * FROM memories WHERE id = $1 AND user_id = $2", [ mockMemoryId, mockUserId, ]); expect(result.rows.length).toBe(0); }); }); describe("Memory Update Flow", () => { it("should regenerate embeddings when content is updated", async () => { const mockMemoryId = "test-memory-update"; // userId context established for the test scenario const newContent = "Updated memory content"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [{ id: mockMemoryId }], rowCount: 1, }); // Begin transaction const client = await mockDb.manager.beginTransaction(); // Update content triggers embedding regeneration const newEmbeddings = await mockEmbeddingEngine.generateAllSectorEmbeddings({ text: newContent, sector: "semantic", }); expect(mockEmbeddingEngine.generateAllSectorEmbeddings).toHaveBeenCalled(); // Store new embeddings await mockEmbeddingStorage.storeEmbeddings(mockMemoryId, newEmbeddings, "default", client); expect(mockEmbeddingStorage.storeEmbeddings).toHaveBeenCalled(); // Commit transaction await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should update waypoint links when content changes significantly", async () => { const mockMemoryId = "test-memory-links"; const newContent = "Significantly different content"; const mockMemory: Memory = { id: mockMemoryId, content: newContent, createdAt: new Date(), lastAccessed: new Date(), accessCount: 0, salience: 0.5, decayRate: 0.01, strength: 1.0, userId: "test-user", sessionId: "test-session", primarySector: "semantic", metadata: { keywords: [], tags: [] }, }; // Delete old links await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Create new links await mockGraphBuilder.createWaypointLinks(mockMemory, []); expect(mockGraphBuilder.createWaypointLinks).toHaveBeenCalled(); }); it("should handle update with empty metadata object (no fields to update)", async () => { // Tests the path where metadata is provided but all fields are undefined // This covers the early return when no metadata fields to update const mockMemoryId = "test-memory-empty-metadata"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Mock existing memory retrieval (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [ { id: mockMemoryId, content: "Test memory for empty metadata update", created_at: mockTimestamp.toISOString(), last_accessed: mockTimestamp.toISOString(), access_count: 0, salience: 0.5, decay_rate: 0.01, strength: 1.0, user_id: "test-user", session_id: "test-session", primary_sector: "semantic", }, ], rowCount: 1, }) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Simulate update with empty metadata object const updateParams = { memoryId: mockMemoryId, userId: "test-user", metadata: {}, // Empty metadata object - no fields to update }; // Verify memory exists const existingMemory = await client.query("SELECT * FROM memories WHERE id = $1", [ updateParams.memoryId, ]); expect(existingMemory.rows.length).toBe(1); // With empty metadata, no metadata update query should be needed // The update should succeed without errors await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should handle update with metadata containing only undefined fields", async () => { // Tests the path where metadata fields are explicitly undefined const mockMemoryId = "test-memory-undefined-fields"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Mock existing memory with initial metadata (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [ { id: mockMemoryId, content: "Test memory for undefined metadata fields", created_at: mockTimestamp.toISOString(), last_accessed: mockTimestamp.toISOString(), access_count: 0, salience: 0.5, decay_rate: 0.01, strength: 1.0, user_id: "test-user", session_id: "test-session", primary_sector: "semantic", keywords: ["initial", "test"], tags: ["tag1"], category: "initial-category", importance: 0.5, }, ], rowCount: 1, }) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Simulate update with metadata containing only undefined fields const updateParams = { memoryId: mockMemoryId, userId: "test-user", metadata: { keywords: undefined, tags: undefined, category: undefined, context: undefined, importance: undefined, isAtomic: undefined, parentId: undefined, }, }; // Verify memory exists with original metadata const existingMemory = await client.query("SELECT * FROM memories WHERE id = $1", [ updateParams.memoryId, ]); expect(existingMemory.rows.length).toBe(1); expect(existingMemory.rows[0].keywords).toEqual(["initial", "test"]); expect(existingMemory.rows[0].tags).toEqual(["tag1"]); expect(existingMemory.rows[0].category).toBe("initial-category"); // With all undefined fields, original metadata should be preserved await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should update only strength without metadata changes", async () => { // Tests that metadata update is skipped when only strength is updated const mockMemoryId = "test-memory-strength-only"; // Mock the UPDATE query to return updated strength (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, strength: 0.7, }, ], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Simulate strength-only update const updateResult = await client.query( "UPDATE memories SET strength = $1 WHERE id = $2 RETURNING id, strength", [0.7, mockMemoryId] ); expect(updateResult.rows[0].strength).toBe(0.7); // No embedding regeneration should occur for strength-only update expect(mockEmbeddingEngine.generateAllSectorEmbeddings).not.toHaveBeenCalled(); await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should update only salience without metadata changes", async () => { // Tests that metadata update is skipped when only salience is updated const mockMemoryId = "test-memory-salience-only"; // Mock the UPDATE query to return updated salience (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, salience: 0.8, }, ], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Simulate salience-only update const updateResult = await client.query( "UPDATE memories SET salience = $1 WHERE id = $2 RETURNING id, salience", [0.8, mockMemoryId] ); expect(updateResult.rows[0].salience).toBe(0.8); // No embedding regeneration should occur for salience-only update expect(mockEmbeddingEngine.generateAllSectorEmbeddings).not.toHaveBeenCalled(); await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); }); describe("Memory Deletion Flow", () => { it("should coordinate soft delete across all components", async () => { const mockMemoryId = "test-memory-delete"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [{ id: mockMemoryId }], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Soft delete sets strength to 0 await client.query("UPDATE memories SET strength = 0 WHERE id = $1", [mockMemoryId]); expect(mockDb.client.query).toHaveBeenCalled(); await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should cascade hard delete to embeddings and links", async () => { const mockMemoryId = "test-memory-hard-delete"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // Delete embeddings await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(mockMemoryId); // Delete links await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Delete memory record await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); await mockDb.manager.commitTransaction(client); }); }); describe("Search Integration Flow", () => { it("should coordinate search across embedding engine and storage", async () => { const searchText = "test search query"; // userId context established for the test scenario // Generate query embedding const queryEmbedding = await mockEmbeddingEngine.generateSemanticEmbedding(searchText); expect(mockEmbeddingEngine.generateSemanticEmbedding).toHaveBeenCalledWith(searchText); // Perform vector similarity search mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "memory-1", similarity: 0.95 }, { memoryId: "memory-2", similarity: 0.85 }, ]); const similarResults = await mockEmbeddingStorage.vectorSimilaritySearch( queryEmbedding, "semantic", 10, 0.5 ); expect(mockEmbeddingStorage.vectorSimilaritySearch).toHaveBeenCalled(); expect(similarResults.length).toBe(2); expect(similarResults[0].similarity).toBeGreaterThan(similarResults[1].similarity); }); it("should handle empty search results gracefully", async () => { const searchText = "no matching content"; // Generate query embedding await mockEmbeddingEngine.generateSemanticEmbedding(searchText); // Return empty results mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([]); const results = await mockEmbeddingStorage.vectorSimilaritySearch( new Array(1536).fill(0.1), "semantic", 10, 0.5 ); expect(results).toEqual([]); }); }); describe("Multi-Sector Embedding Flow", () => { it("should generate and store embeddings for all five sectors", async () => { const content = "Multi-sector test content"; const mockMemoryId = "test-multi-sector"; // Generate all sector embeddings const embeddings = await mockEmbeddingEngine.generateAllSectorEmbeddings({ text: content, sector: "semantic", }); // Verify all sectors are present expect(embeddings).toHaveProperty("episodic"); expect(embeddings).toHaveProperty("semantic"); expect(embeddings).toHaveProperty("procedural"); expect(embeddings).toHaveProperty("emotional"); expect(embeddings).toHaveProperty("reflective"); // Store embeddings const client = await mockDb.manager.beginTransaction(); await mockEmbeddingStorage.storeEmbeddings(mockMemoryId, embeddings, "default", client); expect(mockEmbeddingStorage.storeEmbeddings).toHaveBeenCalledWith( mockMemoryId, embeddings, "default", client ); }); }); describe("Error Handling Integration", () => { it("should handle database connection failure", async () => { mockDb.manager.getConnection.mockRejectedValue(new Error("Connection failed")); await expect(mockDb.manager.getConnection()).rejects.toThrow("Connection failed"); }); it("should handle transaction commit failure", async () => { const client = await mockDb.manager.beginTransaction(); mockDb.manager.commitTransaction.mockRejectedValue(new Error("Commit failed")); await expect(mockDb.manager.commitTransaction(client)).rejects.toThrow("Commit failed"); }); it("should handle embedding storage failure", async () => { mockEmbeddingStorage.storeEmbeddings.mockRejectedValue(new Error("Storage failed")); await expect( mockEmbeddingStorage.storeEmbeddings("test-id", {}, "default", null) ).rejects.toThrow("Storage failed"); }); }); describe("Complex Search Queries", () => { it("should coordinate search with all filters combined", async () => { // Setup mock responses for complex search const mockMemoryId = "test-memory-complex-search"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Mock database query to return filtered results (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, content: "Important semantic memory about TypeScript", created_at: mockTimestamp.toISOString(), last_accessed: mockTimestamp.toISOString(), access_count: 5, salience: 0.7, decay_rate: 0.01, strength: 0.9, user_id: "test-user-1", session_id: "test-session", primary_sector: "semantic", keywords: ["typescript", "programming"], tags: ["important", "tech"], category: "development", importance: 0.9, }, ], rowCount: 1, }); // Mock vector similarity search mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: mockMemoryId, similarity: 0.95 }, ]); // Generate query embedding const queryEmbedding = await mockEmbeddingEngine.generateSemanticEmbedding( "Important semantic memory about TypeScript" ); expect(mockEmbeddingEngine.generateSemanticEmbedding).toHaveBeenCalled(); // Perform vector similarity search const similarResults = await mockEmbeddingStorage.vectorSimilaritySearch( queryEmbedding, "semantic", 10, 0.5 ); expect(similarResults.length).toBe(1); expect(similarResults[0].memoryId).toBe(mockMemoryId); expect(similarResults[0].similarity).toBe(0.95); // Query database with filters const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1 AND m.primary_sector = $2 AND m.strength >= $3 AND m.salience >= $4 AND mm.category = $5 AND mm.keywords && $6 AND mm.tags && $7`, ["test-user-1", "semantic", 0.5, 0.3, "development", ["programming"], ["tech"]] ); expect(result.rows.length).toBe(1); expect(result.rows[0].id).toBe(mockMemoryId); }); it("should handle metadata-only search without text query", async () => { const mockMemoryId = "test-memory-metadata-only"; // Mock database query for metadata-only search (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, content: "Memory with specific tags", tags: ["metadata-only"], category: "test-category", }, ], rowCount: 1, }); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1 AND mm.tags && $2 AND mm.category = $3`, ["test-user-1", ["metadata-only"], "test-category"] ); expect(result.rows.length).toBe(1); expect(result.rows[0].id).toBe(mockMemoryId); // No similarity score since no text query }); it("should return empty result set when no matches", async () => { // Mock empty result (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 0, }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([]); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories WHERE user_id = $1 AND primary_sector = $2`, ["test-user-1", "semantic"] ); expect(result.rows).toHaveLength(0); }); it("should handle search with pagination", async () => { // Mock paginated results - page 1 (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [ { id: "memory-1", content: "Pagination test memory 1" }, { id: "memory-2", content: "Pagination test memory 2" }, ], rowCount: 2, }) // Mock paginated results - page 2 .mockResolvedValueOnce({ rows: [{ id: "memory-3", content: "Pagination test memory 3" }], rowCount: 1, }); const client = await mockDb.manager.getConnection(); // First page const page1 = await client.query( `SELECT * FROM memories WHERE user_id = $1 LIMIT $2 OFFSET $3`, ["test-user-1", 2, 0] ); expect(page1.rows.length).toBe(2); // Second page const page2 = await client.query( `SELECT * FROM memories WHERE user_id = $1 LIMIT $2 OFFSET $3`, ["test-user-1", 2, 2] ); expect(page2.rows.length).toBe(1); // Verify no overlap const ids1 = page1.rows.map((r: { id: string }) => r.id); const ids2 = page2.rows.map((r: { id: string }) => r.id); const overlap = ids1.filter((id: string) => ids2.includes(id)); expect(overlap).toHaveLength(0); }); }); describe("Batch Delete Operations", () => { it("should track failures for non-existent memories in batch delete", async () => { const existingMemoryId = "existing-memory-123"; // Mock: First query finds the existing memory, subsequent queries find nothing (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: existingMemoryId }], rowCount: 1, }) .mockResolvedValueOnce({ rows: [], rowCount: 0, }) .mockResolvedValueOnce({ rows: [], rowCount: 0, }); const client = await mockDb.manager.beginTransaction(); // Simulate batch delete logic const memoryIds = [existingMemoryId, "non-existent-1", "non-existent-2"]; const results = { successCount: 0, failureCount: 0, failures: [] as Array<{ memoryId: string; error: string }>, }; for (const memoryId of memoryIds) { const checkResult = await client.query("SELECT id FROM memories WHERE id = $1", [memoryId]); if (checkResult.rows.length === 0) { results.failureCount++; results.failures.push({ memoryId, error: "Memory not found" }); } else { results.successCount++; } } await mockDb.manager.commitTransaction(client); expect(results.successCount).toBe(1); expect(results.failureCount).toBe(2); expect(results.failures).toHaveLength(2); expect(results.failures[0]).toEqual({ memoryId: "non-existent-1", error: "Memory not found", }); }); it("should handle batch delete with all non-existent IDs", async () => { // Mock: All queries return empty (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 0, }); const memoryIds = ["non-existent-1", "non-existent-2", "non-existent-3"]; const results = { successCount: 0, failureCount: 0, failures: [] as Array<{ memoryId: string; error: string }>, }; const client = await mockDb.manager.beginTransaction(); for (const memoryId of memoryIds) { const checkResult = await client.query("SELECT id FROM memories WHERE id = $1", [memoryId]); if (checkResult.rows.length === 0) { results.failureCount++; results.failures.push({ memoryId, error: "Memory not found" }); } else { results.successCount++; } } await mockDb.manager.commitTransaction(client); expect(results.successCount).toBe(0); expect(results.failureCount).toBe(3); expect(results.failures).toHaveLength(3); }); }); describe("Metadata Filtering Edge Cases", () => { it("should handle search with empty keywords array", async () => { // Mock: Return all memories when keywords filter is empty (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: "memory-1", keywords: ["test", "keyword"] }, { id: "memory-2", keywords: ["other"] }, ], rowCount: 2, }); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1`, ["test-user-1"] ); // Empty keywords array means no keyword filter - returns all expect(result.rows.length).toBeGreaterThanOrEqual(0); }); it("should handle search with only category filter", async () => { const mockMemoryId = "test-memory-category"; (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, content: "Memory for category-only search", category: "unique-category-xyz", }, ], rowCount: 1, }); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1 AND mm.category = $2`, ["test-user-1", "unique-category-xyz"] ); expect(result.rows.length).toBe(1); expect(result.rows[0].id).toBe(mockMemoryId); }); it("should handle multiple metadata filters combined", async () => { const mockMemoryId = "test-memory-all-metadata"; (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: mockMemoryId, keywords: ["keyword1", "keyword2"], tags: ["tag1", "tag2"], category: "category1", }, ], rowCount: 1, }); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1 AND mm.keywords && $2 AND mm.tags && $3 AND mm.category = $4`, ["test-user-1", ["keyword2"], ["tag2"], "category1"] ); expect(result.rows.length).toBe(1); expect(result.rows[0].id).toBe(mockMemoryId); }); it("should handle metadata array overlap with no matches", async () => { // Mock: Return empty when no overlap (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 0, }); const client = await mockDb.manager.getConnection(); const result = await client.query( `SELECT * FROM memories m JOIN memory_metadata mm ON m.id = mm.memory_id WHERE m.user_id = $1 AND mm.keywords && $2 AND mm.tags && $3`, ["test-user-1", ["nonexistent-keyword"], ["nonexistent-tag"]] ); expect(result.rows).toHaveLength(0); }); }); describe("Composite Score Calculation", () => { it("should verify composite score formula", async () => { // Test the composite scoring formula: // 0.6×similarity + 0.2×salience + 0.1×recency + 0.1×linkWeight const scoreComponents = { similarity: 0.8, salience: 0.7, recency: 0.9, linkWeight: 0.5, }; const expectedTotal = 0.6 * scoreComponents.similarity + 0.2 * scoreComponents.salience + 0.1 * scoreComponents.recency + 0.1 * scoreComponents.linkWeight; // 0.6 * 0.8 + 0.2 * 0.7 + 0.1 * 0.9 + 0.1 * 0.5 // = 0.48 + 0.14 + 0.09 + 0.05 // = 0.76 expect(expectedTotal).toBeCloseTo(0.76, 5); // Verify all components are in valid range expect(scoreComponents.similarity).toBeGreaterThanOrEqual(0); expect(scoreComponents.similarity).toBeLessThanOrEqual(1); expect(scoreComponents.salience).toBeGreaterThanOrEqual(0); expect(scoreComponents.salience).toBeLessThanOrEqual(1); expect(scoreComponents.recency).toBeGreaterThanOrEqual(0); expect(scoreComponents.recency).toBeLessThanOrEqual(1); expect(scoreComponents.linkWeight).toBeGreaterThanOrEqual(0); expect(scoreComponents.linkWeight).toBeLessThanOrEqual(1); }); }); describe("Sequential Update Operations", () => { it("should maintain data integrity with sequential updates", async () => { const mockMemoryId = "test-memory-sequential"; // Mock sequential update responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId, content: "Updated by operation 1", strength: 0.8 }], rowCount: 1, }) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId, content: "Updated by operation 2", strength: 0.9 }], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); // First update const update1 = await client.query( "UPDATE memories SET content = $1, strength = $2 WHERE id = $3 RETURNING *", ["Updated by operation 1", 0.8, mockMemoryId] ); expect(update1.rows[0].content).toBe("Updated by operation 1"); expect(update1.rows[0].strength).toBe(0.8); // Second update const update2 = await client.query( "UPDATE memories SET content = $1, strength = $2 WHERE id = $3 RETURNING *", ["Updated by operation 2", 0.9, mockMemoryId] ); expect(update2.rows[0].content).toBe("Updated by operation 2"); expect(update2.rows[0].strength).toBe(0.9); await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); }); });

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