Skip to main content
Glama
memory-deletion.test.ts22.8 kB
/** * Memory Deletion Integration Tests (Mocked) * * Tests the interaction between MemoryRepository, WaypointBuilder, and EmbeddingStorage * for cascade deletion operations using mocks for external dependencies. * * This is an integration test that verifies internal module interactions work correctly, * NOT a test of external service integration. * * Requirements: 12.2, 12. 12.6 */ import type { PoolClient } from "pg"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // 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 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 Deletion Integration (Mocked)", () => { let mockDb: ReturnType<typeof createMockDbManager>; let mockEmbeddingStorage: ReturnType<typeof createMockEmbeddingStorage>; let mockGraphBuilder: ReturnType<typeof createMockGraphBuilder>; beforeEach(() => { vi.clearAllMocks(); mockDb = createMockDbManager(); mockEmbeddingStorage = createMockEmbeddingStorage(); mockGraphBuilder = createMockGraphBuilder(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Cascade Deletion of Embeddings", () => { it("should cascade delete all embeddings when memory is hard deleted", async () => { const mockMemoryId = "test-memory-cascade-embeddings"; // Setup mock to find the memory (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // Delete links .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // Delete metadata .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Step 1: Verify memory exists const memoryCheck = await client.query("SELECT id FROM memories WHERE id = $1", [ mockMemoryId, ]); expect(memoryCheck.rows.length).toBe(1); // Step 2: Delete embeddings await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(mockMemoryId); // Step 3: Delete links await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Step 4: Delete memory record await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); // Step 5: Commit transaction await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalledWith(client); }); it("should delete embeddings for all five sectors", async () => { const mockMemoryId = "test-memory-five-sectors"; // Mock retrieveEmbeddings to return all five sectors mockEmbeddingStorage.retrieveEmbeddings.mockResolvedValue({ episodic: new Array(1536).fill(0.1), semantic: new Array(1536).fill(0.2), procedural: new Array(1536).fill(0.3), emotional: new Array(1536).fill(0.4), reflective: new Array(1536).fill(0.5), }); // Verify embeddings exist before deletion const embeddings = await mockEmbeddingStorage.retrieveEmbeddings(mockMemoryId); expect(embeddings).toHaveProperty("episodic"); expect(embeddings).toHaveProperty("semantic"); expect(embeddings).toHaveProperty("procedural"); expect(embeddings).toHaveProperty("emotional"); expect(embeddings).toHaveProperty("reflective"); // Delete embeddings await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(mockMemoryId); }); }); describe("Cascade Deletion of Connections", () => { it("should cascade delete all waypoint connections when memory is deleted", async () => { const mockMemoryId = "test-memory-cascade-connections"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Verify memory exists const memoryCheck = await client.query("SELECT id FROM memories WHERE id = $1", [ mockMemoryId, ]); expect(memoryCheck.rows.length).toBe(1); // Delete waypoint links await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Commit transaction await mockDb.manager.commitTransaction(client); }); it("should delete bidirectional connections", async () => { const memoryId1 = "test-memory-bidirectional-1"; // memoryId2 represents the other end of bidirectional link (not used directly in test) // When deleting memory1, both directions of links should be removed await mockGraphBuilder.deleteLinksForMemory(memoryId1); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(memoryId1); // The graph builder should handle both source_id and target_id cleanup // This is verified by the mock being called with the memory ID }); it("should not delete connections between other memories", async () => { const memoryIdToDelete = "test-memory-to-delete"; const memoryIdToKeep1 = "test-memory-keep-1"; const memoryIdToKeep2 = "test-memory-keep-2"; // Delete only the specified memory's links await mockGraphBuilder.deleteLinksForMemory(memoryIdToDelete); // Verify only the deleted memory's links were targeted expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(memoryIdToDelete); expect(mockGraphBuilder.deleteLinksForMemory).not.toHaveBeenCalledWith(memoryIdToKeep1); expect(mockGraphBuilder.deleteLinksForMemory).not.toHaveBeenCalledWith(memoryIdToKeep2); }); }); describe("Cascade Deletion of Metadata", () => { it("should cascade delete metadata when memory is deleted", async () => { const mockMemoryId = "test-memory-cascade-metadata"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // Delete metadata .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Verify memory exists await client.query("SELECT id FROM memories WHERE id = $1", [mockMemoryId]); // Delete metadata await client.query("DELETE FROM memory_metadata WHERE memory_id = $1", [mockMemoryId]); expect(mockDb.client.query).toHaveBeenCalled(); // Commit transaction await mockDb.manager.commitTransaction(client); }); }); describe("Soft Delete vs Hard Delete", () => { it("should perform soft delete by setting strength to 0", async () => { const mockMemoryId = "test-memory-soft-delete"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId, strength: 1.0 }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [{ id: mockMemoryId, strength: 0 }], rowCount: 1 }); // After update 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(); // Commit transaction await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should not cascade delete on soft delete", async () => { const mockMemoryId = "test-memory-soft-no-cascade"; // 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 only updates strength await client.query("UPDATE memories SET strength = 0 WHERE id = $1", [mockMemoryId]); // Embeddings should NOT be deleted for soft delete expect(mockEmbeddingStorage.deleteEmbeddings).not.toHaveBeenCalled(); // Links should NOT be deleted for soft delete expect(mockGraphBuilder.deleteLinksForMemory).not.toHaveBeenCalled(); await mockDb.manager.commitTransaction(client); }); it("should perform hard delete by removing record completely", async () => { const mockMemoryId = "test-memory-hard-delete"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Hard delete removes the record await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); await mockDb.manager.commitTransaction(client); }); it("should allow hard delete after soft delete", async () => { const mockMemoryId = "test-memory-soft-then-hard"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId, strength: 0 }], rowCount: 1 }) // Soft deleted memory .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Hard delete const client = await mockDb.manager.beginTransaction(); // Verify soft-deleted memory exists (strength = 0) const softDeleted = await client.query("SELECT * FROM memories WHERE id = $1", [ mockMemoryId, ]); expect(softDeleted.rows[0].strength).toBe(0); // Hard delete the soft-deleted memory await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); await mockDb.manager.commitTransaction(client); }); }); describe("Error Handling", () => { it("should throw error when deleting non-existent memory", async () => { const nonExistentId = "non-existent-memory"; // Setup mock to return empty result (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [], rowCount: 0, }); const client = await mockDb.manager.beginTransaction(); // Check if memory exists const result = await client.query("SELECT id FROM memories WHERE id = $1", [nonExistentId]); expect(result.rows.length).toBe(0); // In real implementation, this would throw MemoryValidationError }); it("should handle deletion of memory with no connections gracefully", async () => { const mockMemoryId = "test-memory-no-connections"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // No links to delete .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Delete links (none exist) await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Delete memory should still succeed await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); await mockDb.manager.commitTransaction(client); }); it("should rollback transaction on deletion failure", async () => { const mockMemoryId = "test-memory-rollback"; // Setup mock to fail during deletion mockEmbeddingStorage.deleteEmbeddings.mockRejectedValue( new Error("Embedding deletion failed") ); const client = await mockDb.manager.beginTransaction(); // Attempt deletion (should fail) await expect(mockEmbeddingStorage.deleteEmbeddings(mockMemoryId)).rejects.toThrow( "Embedding deletion failed" ); // Rollback should be called await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalledWith(client); }); it("should handle empty memoryId validation", async () => { const emptyId = ""; // In real implementation, this would throw MemoryValidationError // Here we verify the validation logic would be triggered expect(emptyId.trim()).toBe(""); }); }); describe("Complete Cascade Deletion", () => { it("should cascade delete all related data in single operation", async () => { const mockMemoryId = "test-memory-complete-cascade"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>) .mockResolvedValueOnce({ rows: [{ id: mockMemoryId }], rowCount: 1 }) // Memory exists .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // Delete metadata .mockResolvedValueOnce({ rows: [], rowCount: 1 }); // Delete memory const client = await mockDb.manager.beginTransaction(); // Step 1: Delete embeddings await mockEmbeddingStorage.deleteEmbeddings(mockMemoryId); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(mockMemoryId); // Step 2: Delete waypoint links await mockGraphBuilder.deleteLinksForMemory(mockMemoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(mockMemoryId); // Step 3: Delete metadata await client.query("DELETE FROM memory_metadata WHERE memory_id = $1", [mockMemoryId]); // Step 4: Delete memory await client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]); // Step 5: Commit await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); }); describe("Batch Deletion", () => { it("should batch hard delete multiple memories", async () => { const memoryIds = ["batch-memory-1", "batch-memory-2", "batch-memory-3"]; // Setup mock responses for each memory (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: memoryIds.map((id) => ({ id })), rowCount: memoryIds.length, }); const client = await mockDb.manager.beginTransaction(); const results = { successCount: 0, failureCount: 0, failures: [] as Array<{ memoryId: string; error: string }>, }; // Process each memory for (const memoryId of memoryIds) { await mockEmbeddingStorage.deleteEmbeddings(memoryId); await mockGraphBuilder.deleteLinksForMemory(memoryId); await client.query("DELETE FROM memories WHERE id = $1", [memoryId]); results.successCount++; } await mockDb.manager.commitTransaction(client); expect(results.successCount).toBe(3); expect(results.failureCount).toBe(0); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledTimes(3); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledTimes(3); }); it("should batch soft delete multiple memories", async () => { const memoryIds = ["soft-batch-1", "soft-batch-2", "soft-batch-3"]; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: memoryIds.map((id) => ({ id, strength: 0 })), rowCount: memoryIds.length, }); const client = await mockDb.manager.beginTransaction(); // Soft delete all memories for (const memoryId of memoryIds) { await client.query("UPDATE memories SET strength = 0 WHERE id = $1", [memoryId]); } await mockDb.manager.commitTransaction(client); // Embeddings and links should NOT be deleted for soft delete expect(mockEmbeddingStorage.deleteEmbeddings).not.toHaveBeenCalled(); expect(mockGraphBuilder.deleteLinksForMemory).not.toHaveBeenCalled(); }); it("should handle batch delete with mixed results", async () => { const validIds = ["valid-1", "valid-2"]; const invalidIds = ["invalid-1", "invalid-2"]; const allIds = [...validIds, ...invalidIds]; // Setup mock to return only valid memories (mockDb.client.query as ReturnType<typeof vi.fn>).mockImplementation( async (_sql: string, params?: unknown[]) => { const id = params?.[0] as string; if (validIds.includes(id)) { return { rows: [{ id }], rowCount: 1 }; } return { rows: [], rowCount: 0 }; } ); const client = await mockDb.manager.beginTransaction(); const results = { successCount: 0, failureCount: 0, failures: [] as Array<{ memoryId: string; error: string }>, }; for (const memoryId of allIds) { 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 { await mockEmbeddingStorage.deleteEmbeddings(memoryId); await mockGraphBuilder.deleteLinksForMemory(memoryId); results.successCount++; } } await mockDb.manager.commitTransaction(client); expect(results.successCount).toBe(2); expect(results.failureCount).toBe(2); expect(results.failures.map((f) => f.memoryId)).toEqual(invalidIds); }); it("should throw error for empty array", async () => { const emptyIds: string[] = []; // In real implementation, this would throw MemoryValidationError expect(emptyIds.length).toBe(0); }); it("should handle batch delete with single memory", async () => { const memoryId = "single-batch-memory"; // Setup mock responses (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [{ id: memoryId }], rowCount: 1, }); const client = await mockDb.manager.beginTransaction(); await mockEmbeddingStorage.deleteEmbeddings(memoryId); await mockGraphBuilder.deleteLinksForMemory(memoryId); await client.query("DELETE FROM memories WHERE id = $1", [memoryId]); await mockDb.manager.commitTransaction(client); expect(mockEmbeddingStorage.deleteEmbeddings).toHaveBeenCalledWith(memoryId); expect(mockGraphBuilder.deleteLinksForMemory).toHaveBeenCalledWith(memoryId); }); }); describe("Transaction Error Handling", () => { it("should wrap non-validation errors in transaction error", async () => { // Test transaction error handling (memoryId context not needed for this test) // Setup mock to fail on begin transaction mockDb.manager.beginTransaction.mockRejectedValue(new Error("Database connection failed")); await expect(mockDb.manager.beginTransaction()).rejects.toThrow("Database connection failed"); }); it("should rollback transaction on error", async () => { const mockMemoryId = "test-rollback-error"; // Setup mock to fail during query (mockDb.client.query as ReturnType<typeof vi.fn>).mockRejectedValue( new Error("Query execution failed") ); const client = await mockDb.manager.beginTransaction(); // Attempt query (should fail) await expect( client.query("DELETE FROM memories WHERE id = $1", [mockMemoryId]) ).rejects.toThrow("Query execution failed"); // Rollback should be called await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalledWith(client); }); }); describe("Batch Delete Error Handling", () => { it("should handle batch delete transaction rollback", async () => { const memoryIds = ["batch-rollback-1", "batch-rollback-2"]; // Setup mock to fail during deletion mockEmbeddingStorage.deleteEmbeddings.mockRejectedValue(new Error("Batch deletion failed")); const client = await mockDb.manager.beginTransaction(); // Attempt batch deletion (should fail) await expect(mockEmbeddingStorage.deleteEmbeddings(memoryIds[0])).rejects.toThrow( "Batch deletion failed" ); // Rollback should be called await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalledWith(client); }); }); });

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