Skip to main content
Glama
SessionManager.integration.test.ts19.6 kB
import * as fs from 'node:fs/promises' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SessionManager } from '../../session/SessionManager' import type { SessionConfig } from '../../types/SessionData' describe('SessionManager', () => { let testSessionDir: string let sessionConfig: SessionConfig beforeEach(async () => { // Create a temporary test directory testSessionDir = path.join(os.tmpdir(), `test-sessions-${Date.now()}`) sessionConfig = { enabled: true, sessionDir: testSessionDir, retentionDays: 7, } }) afterEach(async () => { // Clean up test directory try { await fs.rm(testSessionDir, { recursive: true, force: true }) } catch { // Ignore errors if directory doesn't exist } }) describe('constructor', () => { it('should create a SessionManager instance with valid config', async () => { const manager = new SessionManager(sessionConfig) expect(manager).toBeInstanceOf(SessionManager) // Verify that session directory was created const dirExists = await fs.stat(testSessionDir) expect(dirExists.isDirectory()).toBe(true) }) it('should create session directory if it does not exist', async () => { const newDir = path.join(os.tmpdir(), `new-session-dir-${Date.now()}`) const newConfig: SessionConfig = { enabled: true, sessionDir: newDir, retentionDays: 7, } new SessionManager(newConfig) // Verify that directory was created const dirExists = await fs.stat(newDir) expect(dirExists.isDirectory()).toBe(true) // Cleanup await fs.rm(newDir, { recursive: true, force: true }) }) }) describe('validateSessionId', () => { it('should accept valid session IDs with alphanumeric, hyphens, and underscores', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('abc123')).not.toThrow() expect(() => manager.validateSessionId('abc-123')).not.toThrow() expect(() => manager.validateSessionId('abc_123')).not.toThrow() expect(() => manager.validateSessionId('ABC-123_xyz')).not.toThrow() }) it('should reject session IDs with directory traversal attempts', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('../etc')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('./local')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('../../etc')).toThrow('Invalid session ID') }) it('should reject session IDs with special characters', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('abc@123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('abc/123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('abc\\123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('abc 123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('abc;123')).toThrow('Invalid session ID') }) it('should reject empty session IDs', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('')).toThrow('Invalid session ID') }) }) describe('buildFilePath', () => { it('should build file path with correct naming convention', () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-123' const agentType = 'rule-advisor' const filePath = manager.buildFilePath(sessionId, agentType) // Expected format: [session_id]_[agent_type].json const expectedFileName = `${sessionId}_${agentType}.json` expect(filePath).toBe(path.join(testSessionDir, expectedFileName)) }) it('should prevent directory traversal in session ID', () => { const manager = new SessionManager(sessionConfig) const maliciousSessionId = '../etc/passwd' const agentType = 'rule-advisor' expect(() => manager.buildFilePath(maliciousSessionId, agentType)).toThrow( 'Invalid session ID' ) }) it('should ensure file path is within session directory', () => { const manager = new SessionManager(sessionConfig) const sessionId = 'valid-session' const agentType = 'rule-advisor' const filePath = manager.buildFilePath(sessionId, agentType) // Verify that the resolved path is within the session directory const normalizedFilePath = path.normalize(filePath) const normalizedSessionDir = path.normalize(testSessionDir) expect(normalizedFilePath.startsWith(normalizedSessionDir)).toBe(true) }) }) describe('saveSession', () => { it('should save session data to JSON file with correct naming convention', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-001' const request = { agent: 'rule-advisor', prompt: 'Test prompt', cwd: '/test/dir', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Verify that file was created const files = await fs.readdir(testSessionDir) expect(files.length).toBe(1) // File name should match: [session_id]_[agent_type].json expect(files[0]).toBe('test-session-001_rule-advisor.json') // Verify file content const filePath = path.join(testSessionDir, files[0]) const fileContent = await fs.readFile(filePath, 'utf-8') const sessionData = JSON.parse(fileContent) expect(sessionData.sessionId).toBe(sessionId) expect(sessionData.agentType).toBe('rule-advisor') expect(sessionData.history).toHaveLength(1) expect(sessionData.history[0].request).toEqual(request) expect(sessionData.history[0].response).toEqual(response) expect(sessionData.createdAt).toBeDefined() expect(sessionData.lastUpdatedAt).toBeDefined() }) it('should append to existing session file', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-002' const request1 = { agent: 'rule-advisor', prompt: 'First prompt', } const response1 = { stdout: 'First output', stderr: '', exitCode: 0, executionTime: 100, } const request2 = { agent: 'rule-advisor', prompt: 'Second prompt', } const response2 = { stdout: 'Second output', stderr: '', exitCode: 0, executionTime: 200, } // Save first session await manager.saveSession(sessionId, request1, response1) // Save second session (should append) await manager.saveSession(sessionId, request2, response2) // Verify that there's still only one file const files = await fs.readdir(testSessionDir) const sessionFiles = files.filter((file) => file.startsWith('test-session-002')) expect(sessionFiles.length).toBeGreaterThan(0) // Get the latest file const latestFile = sessionFiles.sort().pop() const filePath = path.join(testSessionDir, latestFile!) const fileContent = await fs.readFile(filePath, 'utf-8') const sessionData = JSON.parse(fileContent) // Verify that both entries are in history expect(sessionData.history.length).toBeGreaterThanOrEqual(2) expect(sessionData.createdAt).toBeDefined() expect(sessionData.lastUpdatedAt).toBeDefined() }) it('should set file permissions to 0o600', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-003' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Get the created file const files = await fs.readdir(testSessionDir) const filePath = path.join(testSessionDir, files[0]) // Check file permissions const stats = await fs.stat(filePath) const mode = stats.mode & 0o777 expect(mode).toBe(0o600) }) it('should not throw error when session save fails', async () => { const manager = new SessionManager(sessionConfig) const invalidSessionId = '../invalid/session' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } // Should not throw error even with invalid session ID await expect( manager.saveSession(invalidSessionId, request, response) ).resolves.toBeUndefined() }) it('should log error when session save fails', async () => { const manager = new SessionManager(sessionConfig) const invalidSessionId = '../invalid' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } // Spy on console.error const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) await manager.saveSession(invalidSessionId, request, response) // Verify that error was logged expect(consoleErrorSpy).toHaveBeenCalled() consoleErrorSpy.mockRestore() }) }) describe('loadSession', () => { it('should load an existing session successfully', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-load-001' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } // First, save a session await manager.saveSession(sessionId, request, response) // Then, load it const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') // Verify the loaded session expect(loadedSession).not.toBeNull() expect(loadedSession?.sessionId).toBe(sessionId) expect(loadedSession?.agentType).toBe('rule-advisor') expect(loadedSession?.history).toHaveLength(1) expect(loadedSession?.history[0].request).toEqual(request) expect(loadedSession?.history[0].response).toEqual(response) expect(loadedSession?.createdAt).toBeInstanceOf(Date) expect(loadedSession?.lastUpdatedAt).toBeInstanceOf(Date) }) it('should return null when session file does not exist', async () => { const manager = new SessionManager(sessionConfig) const nonExistentSessionId = 'non-existent-session' const loadedSession = await manager.loadSession(nonExistentSessionId, 'rule-advisor') expect(loadedSession).toBeNull() }) it('should return null when JSON parse fails', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-invalid-json' // Create a file with invalid JSON const fileName = `${sessionId}_rule-advisor.json` const filePath = path.join(testSessionDir, fileName) await fs.writeFile(filePath, 'invalid json content', 'utf-8') const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession).toBeNull() }) it('should load the most recent session file when multiple files exist', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'test-session-multiple' const request1 = { agent: 'rule-advisor', prompt: 'First prompt', } const response1 = { stdout: 'First output', stderr: '', exitCode: 0, executionTime: 100, } const request2 = { agent: 'rule-advisor', prompt: 'Second prompt', } const response2 = { stdout: 'Second output', stderr: '', exitCode: 0, executionTime: 200, } // Save first session await manager.saveSession(sessionId, request1, response1) // Save second session (appends to the same file) await manager.saveSession(sessionId, request2, response2) // Load session - should get both entries in history const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession).not.toBeNull() expect(loadedSession?.history.length).toBe(2) }) it('should isolate sessions by agent type - CRITICAL for sub-agent isolation', async () => { // Red: This test should FAIL with current implementation // Current bug: loadSession(sessionId) ignores agent_type const manager = new SessionManager(sessionConfig) const sessionId = 'shared-session-001' // Save session for rule-advisor const ruleAdvisorRequest = { agent: 'rule-advisor', prompt: 'Analyze code quality', } const ruleAdvisorResponse = { stdout: 'Rule advisor response', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, ruleAdvisorRequest, ruleAdvisorResponse) // Save session for task-executor (same session_id, different agent) const taskExecutorRequest = { agent: 'task-executor', prompt: 'Execute task', } const taskExecutorResponse = { stdout: 'Task executor response', stderr: '', exitCode: 0, executionTime: 200, } await manager.saveSession(sessionId, taskExecutorRequest, taskExecutorResponse) // Load session for rule-advisor with agent_type parameter const ruleAdvisorSession = await manager.loadSession(sessionId, 'rule-advisor') // Verify isolation: should ONLY get rule-advisor's session, NOT task-executor's expect(ruleAdvisorSession).not.toBeNull() expect(ruleAdvisorSession?.agentType).toBe('rule-advisor') expect(ruleAdvisorSession?.history).toHaveLength(1) expect(ruleAdvisorSession?.history[0].request.prompt).toBe('Analyze code quality') expect(ruleAdvisorSession?.history[0].response.stdout).toBe('Rule advisor response') // Load session for task-executor with agent_type parameter const taskExecutorSession = await manager.loadSession(sessionId, 'task-executor') // Verify isolation: should ONLY get task-executor's session, NOT rule-advisor's expect(taskExecutorSession).not.toBeNull() expect(taskExecutorSession?.agentType).toBe('task-executor') expect(taskExecutorSession?.history).toHaveLength(1) expect(taskExecutorSession?.history[0].request.prompt).toBe('Execute task') expect(taskExecutorSession?.history[0].response.stdout).toBe('Task executor response') }) }) describe('cleanupOldSessions', () => { it('should delete files older than retention days', async () => { const manager = new SessionManager(sessionConfig) // Create test files with different ages const oldFileName = 'old-session_rule-advisor.json' const oldFilePath = path.join(testSessionDir, oldFileName) await fs.writeFile(oldFilePath, JSON.stringify({ test: 'data' }), 'utf-8') // Set file modification time to 8 days ago (older than retention period) const eightDaysAgo = new Date() eightDaysAgo.setDate(eightDaysAgo.getDate() - 8) await fs.utimes(oldFilePath, eightDaysAgo, eightDaysAgo) // Create a recent file (within retention period) const recentFileName = 'recent-session_rule-advisor.json' const recentFilePath = path.join(testSessionDir, recentFileName) await fs.writeFile(recentFilePath, JSON.stringify({ test: 'data' }), 'utf-8') // Execute cleanup await manager.cleanupOldSessions() // Verify old file was deleted const files = await fs.readdir(testSessionDir) expect(files).not.toContain(oldFileName) expect(files).toContain(recentFileName) }) it('should not delete files within retention period', async () => { const manager = new SessionManager(sessionConfig) // Create a file that's 3 days old (within 7-day retention) const fileName = 'test-session_rule-advisor.json' const filePath = path.join(testSessionDir, fileName) await fs.writeFile(filePath, JSON.stringify({ test: 'data' }), 'utf-8') // Set file modification time to 3 days ago const threeDaysAgo = new Date() threeDaysAgo.setDate(threeDaysAgo.getDate() - 3) await fs.utimes(filePath, threeDaysAgo, threeDaysAgo) // Execute cleanup await manager.cleanupOldSessions() // Verify file still exists const files = await fs.readdir(testSessionDir) expect(files).toContain(fileName) }) it('should not throw error when cleanup fails', async () => { const manager = new SessionManager(sessionConfig) // Create a file const fileName = 'test-session_rule-advisor.json' const filePath = path.join(testSessionDir, fileName) await fs.writeFile(filePath, JSON.stringify({ test: 'data' }), 'utf-8') // Set to old date const eightDaysAgo = new Date() eightDaysAgo.setDate(eightDaysAgo.getDate() - 8) await fs.utimes(filePath, eightDaysAgo, eightDaysAgo) // Make file read-only to simulate delete failure await fs.chmod(filePath, 0o444) // Spy on console.error to verify error logging const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Should not throw error even if deletion fails await expect(manager.cleanupOldSessions()).resolves.toBeUndefined() consoleErrorSpy.mockRestore() // Try to restore permissions for cleanup (may fail if file was deleted) try { await fs.chmod(filePath, 0o644) } catch { // Ignore if file was already deleted } }) it('should delete multiple old files in a single cleanup', async () => { const manager = new SessionManager(sessionConfig) // Create two old files (different session IDs) const oldFile1 = 'old-session-1_rule-advisor.json' const oldFile2 = 'old-session-2_rule-advisor.json' const oldFilePath1 = path.join(testSessionDir, oldFile1) const oldFilePath2 = path.join(testSessionDir, oldFile2) await fs.writeFile(oldFilePath1, JSON.stringify({ test: 'data' }), 'utf-8') await fs.writeFile(oldFilePath2, JSON.stringify({ test: 'data' }), 'utf-8') // Set both files to 8 days ago const eightDaysAgo = new Date() eightDaysAgo.setDate(eightDaysAgo.getDate() - 8) await fs.utimes(oldFilePath1, eightDaysAgo, eightDaysAgo) await fs.utimes(oldFilePath2, eightDaysAgo, eightDaysAgo) // Execute cleanup await manager.cleanupOldSessions() // Assert - focus on behavior: both old files are deleted const remainingFiles = await fs.readdir(testSessionDir) expect(remainingFiles).not.toContain(oldFile1) expect(remainingFiles).not.toContain(oldFile2) }) }) })

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/shinpr/sub-agents-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server