UNPKG

@emmahyde/thinking-patterns

Version:

MCP server combining systematic thinking, mental models, debugging approaches, and stochastic algorithms for comprehensive cognitive pattern support

426 lines (425 loc) 20.2 kB
/** * Tests for SessionManager interface and InMemorySessionManager implementation * Tests session lifecycle management, state persistence, and cleanup */ import { InMemorySessionManager, sessionManager } from '../../src/services/SessionManager.js'; import { resetAllMocks } from '../helpers/mockFactories.js'; describe('InMemorySessionManager', () => { let manager; beforeEach(() => { resetAllMocks(); manager = new InMemorySessionManager(); }); afterEach(() => { manager.destroy(); }); describe('constructor', () => { it('should initialize with empty sessions map', () => { expect(manager.getSessionCount()).toBe(0); }); it('should set up cleanup interval', () => { expect(manager['cleanupInterval']).toBeDefined(); expect(typeof manager['cleanupInterval']).toBe('object'); }); }); describe('createSession', () => { it('should create a new session with empty history', () => { const sessionId = 'test-session-1'; manager.createSession(sessionId); const session = manager.getSession(sessionId); expect(session).not.toBeNull(); expect(session.thoughtHistory).toEqual([]); expect(session.branches).toEqual({}); expect(session.createdAt).toBeInstanceOf(Date); expect(session.lastAccessedAt).toBeInstanceOf(Date); }); it('should create multiple sessions independently', () => { manager.createSession('session-1'); manager.createSession('session-2'); expect(manager.getSessionCount()).toBe(2); const session1 = manager.getSession('session-1'); const session2 = manager.getSession('session-2'); expect(session1).not.toBe(session2); expect(session1.thoughtHistory).toEqual([]); expect(session2.thoughtHistory).toEqual([]); }); it('should overwrite existing session with same ID', async () => { const sessionId = 'test-session'; manager.createSession(sessionId); const originalSession = manager.getSession(sessionId); // Add some data to the original session manager.addThought(sessionId, { thought: "test thought", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }); // Wait a moment to ensure different timestamp (increased for CI reliability) await new Promise(resolve => setTimeout(resolve, 10)); // Create new session with same ID manager.createSession(sessionId); const newSession = manager.getSession(sessionId); expect(newSession.thoughtHistory).toHaveLength(0); expect(newSession.createdAt.getTime()).toBeGreaterThan(originalSession.createdAt.getTime()); }); }); describe('getSession', () => { it('should return null for non-existent session', () => { const session = manager.getSession('non-existent'); expect(session).toBeNull(); }); it('should return existing session and update lastAccessedAt', async () => { const sessionId = 'test-session'; manager.createSession(sessionId); const originalTime = manager.getSession(sessionId).lastAccessedAt; // Wait a bit to ensure time difference await new Promise(resolve => setTimeout(resolve, 10)); const session = manager.getSession(sessionId); expect(session).not.toBeNull(); expect(session.lastAccessedAt.getTime()).toBeGreaterThan(originalTime.getTime()); }); it('should return session data with correct structure', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const session = manager.getSession(sessionId); expect(session).toHaveProperty('thoughtHistory'); expect(session).toHaveProperty('branches'); expect(session).toHaveProperty('createdAt'); expect(session).toHaveProperty('lastAccessedAt'); expect(Array.isArray(session.thoughtHistory)).toBe(true); expect(typeof session.branches).toBe('object'); }); }); describe('clearSession', () => { it('should remove existing session', () => { const sessionId = 'test-session'; manager.createSession(sessionId); expect(manager.getSession(sessionId)).not.toBeNull(); manager.clearSession(sessionId); expect(manager.getSession(sessionId)).toBeNull(); expect(manager.getSessionCount()).toBe(0); }); it('should not throw error for non-existent session', () => { expect(() => manager.clearSession('non-existent')).not.toThrow(); }); it('should only remove specified session', () => { manager.createSession('session-1'); manager.createSession('session-2'); manager.clearSession('session-1'); expect(manager.getSession('session-1')).toBeNull(); expect(manager.getSession('session-2')).not.toBeNull(); expect(manager.getSessionCount()).toBe(1); }); }); describe('addThought', () => { it('should add thought to existing session', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const thought = { thought: "Test thought", thoughtNumber: 1, totalThoughts: 3, nextThoughtNeeded: true }; manager.addThought(sessionId, thought); const session = manager.getSession(sessionId); expect(session.thoughtHistory).toHaveLength(1); expect(session.thoughtHistory[0]).toEqual(thought); }); it('should create session if it does not exist', () => { const sessionId = 'new-session'; const thought = { thought: "Test thought", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; manager.addThought(sessionId, thought); const session = manager.getSession(sessionId); expect(session).not.toBeNull(); expect(session.thoughtHistory).toHaveLength(1); expect(session.thoughtHistory[0]).toEqual(thought); }); it('should add multiple thoughts in order', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const thoughts = [ { thought: "First thought", thoughtNumber: 1, totalThoughts: 3, nextThoughtNeeded: true }, { thought: "Second thought", thoughtNumber: 2, totalThoughts: 3, nextThoughtNeeded: true }, { thought: "Third thought", thoughtNumber: 3, totalThoughts: 3, nextThoughtNeeded: false } ]; thoughts.forEach(thought => manager.addThought(sessionId, thought)); const session = manager.getSession(sessionId); expect(session.thoughtHistory).toHaveLength(3); expect(session.thoughtHistory).toEqual(thoughts); }); it('should handle optional thought properties', () => { const sessionId = 'test-session'; const thought = { thought: "Revision thought", thoughtNumber: 2, totalThoughts: 3, nextThoughtNeeded: true, isRevision: true, revisesThought: 1, branchFromThought: 1, branchId: "branch-1" }; manager.addThought(sessionId, thought); const session = manager.getSession(sessionId); expect(session.thoughtHistory[0]).toEqual(thought); }); }); describe('addBranch', () => { it('should add branch to existing session', () => { const sessionId = 'test-session'; const branchId = 'branch-1'; manager.createSession(sessionId); const thought = { thought: "Branch thought", thoughtNumber: 1, totalThoughts: 2, nextThoughtNeeded: true, branchFromThought: 1, branchId: branchId }; manager.addBranch(sessionId, branchId, thought); const session = manager.getSession(sessionId); expect(session.branches[branchId]).toHaveLength(1); expect(session.branches[branchId][0]).toEqual(thought); }); it('should create session if it does not exist', () => { const sessionId = 'new-session'; const branchId = 'branch-1'; const thought = { thought: "Branch thought", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; manager.addBranch(sessionId, branchId, thought); const session = manager.getSession(sessionId); expect(session).not.toBeNull(); expect(session.branches[branchId]).toHaveLength(1); }); it('should add multiple thoughts to same branch', () => { const sessionId = 'test-session'; const branchId = 'branch-1'; manager.createSession(sessionId); const thoughts = [ { thought: "Branch thought 1", thoughtNumber: 1, totalThoughts: 2, nextThoughtNeeded: true }, { thought: "Branch thought 2", thoughtNumber: 2, totalThoughts: 2, nextThoughtNeeded: false } ]; thoughts.forEach(thought => manager.addBranch(sessionId, branchId, thought)); const session = manager.getSession(sessionId); expect(session.branches[branchId]).toHaveLength(2); expect(session.branches[branchId]).toEqual(thoughts); }); it('should handle multiple branches independently', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const branch1Thought = { thought: "Branch 1", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; const branch2Thought = { thought: "Branch 2", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; manager.addBranch(sessionId, 'branch-1', branch1Thought); manager.addBranch(sessionId, 'branch-2', branch2Thought); const session = manager.getSession(sessionId); expect(Object.keys(session.branches)).toHaveLength(2); expect(session.branches['branch-1'][0]).toEqual(branch1Thought); expect(session.branches['branch-2'][0]).toEqual(branch2Thought); }); }); describe('getThoughtHistory', () => { it('should return empty array for non-existent session', () => { const history = manager.getThoughtHistory('non-existent'); expect(history).toEqual([]); }); it('should return thought history for existing session', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const thoughts = [ { thought: "First", thoughtNumber: 1, totalThoughts: 2, nextThoughtNeeded: true }, { thought: "Second", thoughtNumber: 2, totalThoughts: 2, nextThoughtNeeded: false } ]; thoughts.forEach(thought => manager.addThought(sessionId, thought)); const history = manager.getThoughtHistory(sessionId); expect(history).toEqual(thoughts); }); it('should return copy of history array', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const thought = { thought: "Test", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; manager.addThought(sessionId, thought); const history1 = manager.getThoughtHistory(sessionId); const history2 = manager.getThoughtHistory(sessionId); expect(history1).toEqual(history2); expect(history1).not.toBe(history2); // Different array instances }); }); describe('getBranches', () => { it('should return empty object for non-existent session', () => { const branches = manager.getBranches('non-existent'); expect(branches).toEqual({}); }); it('should return branches for existing session', () => { const sessionId = 'test-session'; manager.createSession(sessionId); const thought1 = { thought: "Branch 1", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; const thought2 = { thought: "Branch 2", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }; manager.addBranch(sessionId, 'branch-1', thought1); manager.addBranch(sessionId, 'branch-2', thought2); const branches = manager.getBranches(sessionId); expect(Object.keys(branches)).toHaveLength(2); expect(branches['branch-1']).toEqual([thought1]); expect(branches['branch-2']).toEqual([thought2]); }); }); describe('cleanupExpiredSessions', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should remove sessions older than timeout', () => { const sessionId = 'old-session'; manager.createSession(sessionId); // Fast-forward time beyond session timeout (1 hour) vi.advanceTimersByTime(61 * 60 * 1000); manager.cleanupExpiredSessions(); expect(manager.getSession(sessionId)).toBeNull(); expect(manager.getSessionCount()).toBe(0); }); it('should keep sessions within timeout window', () => { const sessionId = 'recent-session'; manager.createSession(sessionId); // Fast-forward time but still within timeout (59 minutes) vi.advanceTimersByTime(59 * 60 * 1000); manager.cleanupExpiredSessions(); expect(manager.getSession(sessionId)).not.toBeNull(); expect(manager.getSessionCount()).toBe(1); }); it('should update lastAccessedAt on getSession to prevent cleanup', () => { const sessionId = 'accessed-session'; manager.createSession(sessionId); // Fast-forward time close to timeout vi.advanceTimersByTime(59 * 60 * 1000); // Access the session to update lastAccessedAt manager.getSession(sessionId); // Fast-forward more time vi.advanceTimersByTime(30 * 60 * 1000); manager.cleanupExpiredSessions(); expect(manager.getSession(sessionId)).not.toBeNull(); }); it('should handle multiple sessions with different ages', () => { manager.createSession('old-session-1'); manager.createSession('old-session-2'); // Fast-forward past timeout vi.advanceTimersByTime(61 * 60 * 1000); manager.createSession('new-session'); manager.cleanupExpiredSessions(); expect(manager.getSession('old-session-1')).toBeNull(); expect(manager.getSession('old-session-2')).toBeNull(); expect(manager.getSession('new-session')).not.toBeNull(); expect(manager.getSessionCount()).toBe(1); }); }); describe('destroy', () => { it('should clear all sessions', () => { manager.createSession('session-1'); manager.createSession('session-2'); expect(manager.getSessionCount()).toBe(2); manager.destroy(); expect(manager.getSessionCount()).toBe(0); }); it('should clear cleanup interval', () => { const intervalSpy = vi.spyOn(global, 'clearInterval'); manager.destroy(); expect(intervalSpy).toHaveBeenCalledWith(manager['cleanupInterval']); }); }); describe('utility methods', () => { describe('getSessionCount', () => { it('should return correct session count', () => { expect(manager.getSessionCount()).toBe(0); manager.createSession('session-1'); expect(manager.getSessionCount()).toBe(1); manager.createSession('session-2'); expect(manager.getSessionCount()).toBe(2); manager.clearSession('session-1'); expect(manager.getSessionCount()).toBe(1); }); }); describe('getSessionInfo', () => { it('should return empty array when no sessions', () => { const info = manager.getSessionInfo(); expect(info).toEqual([]); }); it('should return session information', () => { manager.createSession('session-1'); manager.addThought('session-1', { thought: "Test thought", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }); manager.addBranch('session-1', 'branch-1', { thought: "Branch thought", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }); const info = manager.getSessionInfo(); expect(info).toHaveLength(1); expect(info[0]).toHaveProperty('sessionId', 'session-1'); expect(info[0]).toHaveProperty('thoughtCount', 1); expect(info[0]).toHaveProperty('branchCount', 1); expect(info[0]).toHaveProperty('lastAccessed'); expect(info[0].lastAccessed).toBeInstanceOf(Date); }); it('should return information for multiple sessions', () => { manager.createSession('session-1'); manager.createSession('session-2'); manager.addThought('session-1', { thought: "Thought 1", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false }); const info = manager.getSessionInfo(); expect(info).toHaveLength(2); expect(info.find(s => s.sessionId === 'session-1').thoughtCount).toBe(1); expect(info.find(s => s.sessionId === 'session-2').thoughtCount).toBe(0); }); }); }); describe('singleton instance', () => { it('should provide a default singleton instance', () => { expect(sessionManager).toBeInstanceOf(InMemorySessionManager); }); it('should be the same instance on multiple imports', () => { const anotherReference = sessionManager; expect(sessionManager).toBe(anotherReference); }); }); describe('automatic cleanup interval', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should run cleanup automatically every 15 minutes', () => { // Create a new manager with fake timers already in effect const testManager = new InMemorySessionManager(); const cleanupSpy = vi.spyOn(testManager, 'cleanupExpiredSessions'); // Fast-forward to 15 minutes vi.advanceTimersByTime(15 * 60 * 1000); expect(cleanupSpy).toHaveBeenCalledTimes(1); // Fast-forward another 15 minutes vi.advanceTimersByTime(15 * 60 * 1000); expect(cleanupSpy).toHaveBeenCalledTimes(2); // Clean up testManager.destroy(); }); }); });