Skip to main content
Glama
temporal-decay.test.ts19.3 kB
/** * Temporal Decay Integration Tests (Mocked) * * Tests the interaction between TemporalDecayEngine, MemoryRepository, and BackgroundScheduler * using mocks for external dependencies (database). * * 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, 12.6 */ import type { PoolClient } from "pg"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Memory } 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 sector config manager function createMockSectorConfigManager() { return { getConfig: vi.fn().mockReturnValue({ minimumStrength: 0.01, reinforcementBoost: 0.3, pruningThreshold: 0.1, }), getEffectiveDecayRate: vi.fn().mockReturnValue(0.03), getSectorMultiplier: vi.fn().mockReturnValue(1.0), }; } // Create mock decay engine function createMockDecayEngine() { return { calculateDecayedStrength: vi.fn().mockImplementation((memory: Memory, currentTime: Date) => { const ageMs = currentTime.getTime() - memory.lastAccessed.getTime(); const ageDays = ageMs / (1000 * 60 * 60 * 24); if (ageDays < 0) return memory.strength; const lambda = 0.03; return Math.max(memory.strength * Math.exp(-lambda * ageDays), 0.01); }), applyDecay: vi.fn().mockResolvedValue(undefined), batchApplyDecay: vi.fn().mockResolvedValue(undefined), reinforceMemory: vi.fn().mockResolvedValue(undefined), autoReinforceOnAccess: vi.fn().mockResolvedValue(undefined), getReinforcementHistory: vi.fn().mockResolvedValue([]), reinforceMemoryByType: vi.fn().mockResolvedValue(undefined), scheduleDecayJob: vi.fn(), runDecayMaintenance: vi.fn().mockResolvedValue({ processedCount: 0, prunedCount: 0, processingTime: 100, errors: [], }), identifyPruningCandidates: vi.fn().mockResolvedValue([]), pruneMemories: vi.fn().mockResolvedValue(0), }; } // Create mock memory repository function createMockMemoryRepository() { return { create: vi.fn().mockResolvedValue({ id: "test-memory-1" }), retrieve: vi.fn().mockResolvedValue(null), update: vi.fn().mockResolvedValue({ id: "test-memory-1" }), delete: vi.fn().mockResolvedValue(true), search: vi.fn().mockResolvedValue([]), }; } // Helper to create test memory function createTestMemory(overrides: Partial<Memory> = {}): Memory { const now = new Date(); return { id: "test-memory-1", content: "Test memory content", createdAt: now, lastAccessed: now, accessCount: 1, salience: 0.5, decayRate: 0.03, strength: 1.0, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: {}, ...overrides, }; } describe("Temporal Decay Integration (Mocked)", () => { let mockDb: ReturnType<typeof createMockDbManager>; let mockDecayEngine: ReturnType<typeof createMockDecayEngine>; // Memory repository and config manager are created for context beforeEach(() => { vi.clearAllMocks(); mockDb = createMockDbManager(); mockDecayEngine = createMockDecayEngine(); createMockMemoryRepository(); // Create for context createMockSectorConfigManager(); // Create for context }); afterEach(() => { vi.restoreAllMocks(); }); describe("Decay Engine + Memory Repository Integration", () => { it("should apply decay to memory and update repository", async () => { const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); const memory = createTestMemory({ id: "test-decay-1", lastAccessed: twoDaysAgo, strength: 1.0, }); // Mock database query to return memory (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [ { id: memory.id, strength: memory.strength, }, ], rowCount: 1, }); // Apply decay await mockDecayEngine.applyDecay(memory); expect(mockDecayEngine.applyDecay).toHaveBeenCalledWith(memory); // Calculate expected decayed strength const currentTime = new Date(); const decayedStrength = mockDecayEngine.calculateDecayedStrength(memory, currentTime); expect(decayedStrength).toBeLessThan(1.0); expect(decayedStrength).toBeGreaterThan(0.9); // 2 days = minimal decay }); it("should handle batch decay across multiple memories", async () => { const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); const memories: Memory[] = [1, 2, 3].map((i) => createTestMemory({ id: `test-batch-${i}`, lastAccessed: twoDaysAgo, strength: 1.0, }) ); // Mock database transaction const client = await mockDb.manager.beginTransaction(); expect(mockDb.manager.beginTransaction).toHaveBeenCalled(); // Apply batch decay await mockDecayEngine.batchApplyDecay(memories); expect(mockDecayEngine.batchApplyDecay).toHaveBeenCalledWith(memories); // Commit transaction await mockDb.manager.commitTransaction(client); expect(mockDb.manager.commitTransaction).toHaveBeenCalled(); }); it("should handle empty batch gracefully", async () => { await mockDecayEngine.batchApplyDecay([]); expect(mockDecayEngine.batchApplyDecay).toHaveBeenCalledWith([]); }); it("should rollback transaction on batch error", async () => { const memories: Memory[] = [createTestMemory({ id: "error-memory" })]; // Mock batch apply to throw error mockDecayEngine.batchApplyDecay.mockRejectedValueOnce(new Error("Batch processing failed")); const client = await mockDb.manager.beginTransaction(); await expect(mockDecayEngine.batchApplyDecay(memories)).rejects.toThrow( "Batch processing failed" ); // Rollback should be called await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalled(); }); }); describe("Reinforcement System Integration", () => { it("should reinforce memory and update strength", async () => { const memory = createTestMemory({ id: "test-reinforce-1", strength: 0.6, }); // Mock database query to return memory (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [{ id: memory.id, strength: memory.strength }], rowCount: 1, }); // Reinforce with boost of 0.2 await mockDecayEngine.reinforceMemory(memory.id, 0.2); expect(mockDecayEngine.reinforceMemory).toHaveBeenCalledWith(memory.id, 0.2); }); it("should auto-reinforce on memory access", async () => { const memory = createTestMemory({ id: "test-auto-reinforce", strength: 0.7, }); // Mock database query (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: [{ id: memory.id, strength: memory.strength, access_count: 1 }], rowCount: 1, }); // Auto-reinforce await mockDecayEngine.autoReinforceOnAccess(memory.id); expect(mockDecayEngine.autoReinforceOnAccess).toHaveBeenCalledWith(memory.id); }); it("should track reinforcement history", async () => { const memoryId = "test-history-1"; // Mock reinforcement history mockDecayEngine.getReinforcementHistory.mockResolvedValue([ { timestamp: new Date(), type: "access", boost: 0.3, strengthBefore: 0.5, strengthAfter: 0.8, }, ]); const history = await mockDecayEngine.getReinforcementHistory(memoryId); expect(history).toHaveLength(1); expect(history[0]).toHaveProperty("timestamp"); expect(history[0]).toHaveProperty("type"); expect(history[0]).toHaveProperty("boost"); }); it("should support different reinforcement types", async () => { const memoryId = "test-type-reinforce"; // Test access type await mockDecayEngine.reinforceMemoryByType(memoryId, "access"); expect(mockDecayEngine.reinforceMemoryByType).toHaveBeenCalledWith(memoryId, "access"); // Test explicit type with boost await mockDecayEngine.reinforceMemoryByType(memoryId, "explicit", 0.4); expect(mockDecayEngine.reinforceMemoryByType).toHaveBeenCalledWith(memoryId, "explicit", 0.4); // Test importance type await mockDecayEngine.reinforceMemoryByType(memoryId, "importance"); expect(mockDecayEngine.reinforceMemoryByType).toHaveBeenCalledWith(memoryId, "importance"); }); it("should throw error for non-existent memory reinforcement", async () => { mockDecayEngine.reinforceMemory.mockRejectedValueOnce(new Error("Memory not found")); await expect(mockDecayEngine.reinforceMemory("non-existent", 0.1)).rejects.toThrow( "Memory not found" ); }); }); describe("Pruning System Integration", () => { it("should identify memories below pruning threshold", async () => { // Mock pruning candidates mockDecayEngine.identifyPruningCandidates.mockResolvedValue([ "weak-memory-1", "weak-memory-2", ]); const candidates = await mockDecayEngine.identifyPruningCandidates(0.1); expect(candidates).toContain("weak-memory-1"); expect(candidates).toContain("weak-memory-2"); }); it("should prune memories by IDs", async () => { const memoryIds = ["prune-1", "prune-2"]; // Mock prune operation mockDecayEngine.pruneMemories.mockResolvedValue(2); const deletedCount = await mockDecayEngine.pruneMemories(memoryIds); expect(deletedCount).toBe(2); expect(mockDecayEngine.pruneMemories).toHaveBeenCalledWith(memoryIds); }); it("should handle empty prune list", async () => { mockDecayEngine.pruneMemories.mockResolvedValue(0); const deletedCount = await mockDecayEngine.pruneMemories([]); expect(deletedCount).toBe(0); }); it("should rollback on prune error", async () => { mockDecayEngine.pruneMemories.mockRejectedValueOnce(new Error("Delete operation failed")); const client = await mockDb.manager.beginTransaction(); await expect(mockDecayEngine.pruneMemories(["error-id"])).rejects.toThrow( "Delete operation failed" ); await mockDb.manager.rollbackTransaction(client); expect(mockDb.manager.rollbackTransaction).toHaveBeenCalled(); }); }); describe("Maintenance Cycle Integration", () => { it("should run full maintenance cycle", async () => { // Mock maintenance result mockDecayEngine.runDecayMaintenance.mockResolvedValue({ processedCount: 100, prunedCount: 5, processingTime: 500, errors: [], }); const result = await mockDecayEngine.runDecayMaintenance(); expect(result.processedCount).toBe(100); expect(result.prunedCount).toBe(5); expect(result.processingTime).toBeGreaterThan(0); expect(result.errors).toHaveLength(0); }); it("should handle maintenance errors gracefully", async () => { mockDecayEngine.runDecayMaintenance.mockResolvedValue({ processedCount: 50, prunedCount: 0, processingTime: 200, errors: ["Batch 2 failed: Connection timeout"], }); const result = await mockDecayEngine.runDecayMaintenance(); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors[0]).toContain("failed"); }); it("should validate cron expression for scheduling", () => { // Valid cron expression expect(() => mockDecayEngine.scheduleDecayJob("0 2 * * *")).not.toThrow(); expect(mockDecayEngine.scheduleDecayJob).toHaveBeenCalledWith("0 2 * * *"); }); }); describe("Background Scheduler Integration", () => { it("should coordinate batch processing with decay engine", async () => { // Mock memories for batch processing const memories: Memory[] = Array.from({ length: 100 }, (_, i) => createTestMemory({ id: `batch-${i}`, lastAccessed: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), }) ); // Mock database query to return memories (mockDb.client.query as ReturnType<typeof vi.fn>).mockResolvedValue({ rows: memories.map((m) => ({ id: m.id, content: m.content, created_at: m.createdAt.toISOString(), last_accessed: m.lastAccessed.toISOString(), access_count: m.accessCount, salience: m.salience, decay_rate: m.decayRate, strength: m.strength, user_id: m.userId, session_id: m.sessionId, primary_sector: m.primarySector, })), rowCount: memories.length, }); // Run maintenance mockDecayEngine.runDecayMaintenance.mockResolvedValue({ processedCount: memories.length, prunedCount: 0, processingTime: 1000, errors: [], }); const result = await mockDecayEngine.runDecayMaintenance(); expect(result.processedCount).toBe(100); }); it("should track processing time for maintenance operations", async () => { mockDecayEngine.runDecayMaintenance.mockResolvedValue({ processedCount: 50, prunedCount: 2, processingTime: 750, errors: [], }); const result = await mockDecayEngine.runDecayMaintenance(); expect(result).toHaveProperty("processingTime"); expect(result.processingTime).toBeGreaterThan(0); }); it("should complete processing within time limit", async () => { const maxProcessingTime = 30 * 60 * 1000; // 30 minutes mockDecayEngine.runDecayMaintenance.mockResolvedValue({ processedCount: 1000, prunedCount: 10, processingTime: 5000, // 5 seconds errors: [], }); const result = await mockDecayEngine.runDecayMaintenance(); expect(result.processingTime).toBeLessThan(maxProcessingTime); }); }); describe("Decay Calculation Integration", () => { it("should calculate correct decay for different time periods", () => { const now = new Date(); // 1 day old memory const oneDayOld = createTestMemory({ lastAccessed: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000), strength: 1.0, }); const strength1Day = mockDecayEngine.calculateDecayedStrength(oneDayOld, now); expect(strength1Day).toBeLessThan(1.0); expect(strength1Day).toBeGreaterThan(0.95); // 7 days old memory const sevenDaysOld = createTestMemory({ lastAccessed: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), strength: 1.0, }); const strength7Days = mockDecayEngine.calculateDecayedStrength(sevenDaysOld, now); expect(strength7Days).toBeLessThan(strength1Day); // 30 days old memory const thirtyDaysOld = createTestMemory({ lastAccessed: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), strength: 1.0, }); const strength30Days = mockDecayEngine.calculateDecayedStrength(thirtyDaysOld, now); expect(strength30Days).toBeLessThan(strength7Days); }); it("should handle future lastAccessed (negative time)", () => { const futureTime = new Date("2025-12-12T12:00:00Z"); const currentTime = new Date("2024-12-11T12:00:00Z"); const memory = createTestMemory({ lastAccessed: futureTime, strength: 0.8, }); const strength = mockDecayEngine.calculateDecayedStrength(memory, currentTime); expect(strength).toBe(0.8); // Should return original strength }); it("should enforce minimum strength floor", () => { const veryOldMemory = createTestMemory({ lastAccessed: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // 1 year old strength: 1.0, }); const strength = mockDecayEngine.calculateDecayedStrength(veryOldMemory, new Date()); expect(strength).toBeGreaterThanOrEqual(0.01); // Minimum strength floor }); }); describe("Error Handling Integration", () => { it("should handle database connection failure during decay", async () => { mockDb.manager.getConnection.mockRejectedValueOnce(new Error("Connection failed")); await expect(mockDb.manager.getConnection()).rejects.toThrow("Connection failed"); }); it("should handle transaction failure during batch processing", async () => { mockDb.manager.beginTransaction.mockRejectedValueOnce(new Error("Transaction failed")); await expect(mockDb.manager.beginTransaction()).rejects.toThrow("Transaction failed"); }); it("should handle invalid reinforcement type", async () => { mockDecayEngine.reinforceMemoryByType.mockRejectedValueOnce( new Error("Invalid reinforcement type") ); await expect( mockDecayEngine.reinforceMemoryByType("test-id", "invalid" as "access") ).rejects.toThrow("Invalid reinforcement type"); }); it("should handle explicit reinforcement without boost", async () => { mockDecayEngine.reinforceMemoryByType.mockRejectedValueOnce( new Error("Boost parameter required for explicit reinforcement") ); await expect( mockDecayEngine.reinforceMemoryByType("test-id", "explicit", undefined) ).rejects.toThrow("Boost parameter required"); }); }); describe("Sector-Specific Decay Integration", () => { it("should apply different decay rates per sector", () => { const now = new Date(); const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); // Create memories for different sectors const episodicMemory = createTestMemory({ primarySector: "episodic", lastAccessed: tenDaysAgo, strength: 1.0, }); const semanticMemory = createTestMemory({ primarySector: "semantic", lastAccessed: tenDaysAgo, strength: 1.0, }); // Both should decay (using same mock rate for simplicity) const episodicStrength = mockDecayEngine.calculateDecayedStrength(episodicMemory, now); const semanticStrength = mockDecayEngine.calculateDecayedStrength(semanticMemory, now); expect(episodicStrength).toBeLessThan(1.0); expect(semanticStrength).toBeLessThan(1.0); }); }); });

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