Skip to main content
Glama
session-error-handling.test.ts17.7 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' /** * Error handling tests for session management feature. * * These tests verify that the system continues to operate correctly * even when errors occur, following the error isolation principle: * - Session save failures do not block main execution * - Session load failures return null gracefully * - File system errors are handled gracefully * - Session history formatting uses Markdown for optimal LLM context */ describe('Session Management - Error Handling Tests', () => { let testSessionDir: string let sessionConfig: SessionConfig beforeEach(async () => { // Create a temporary test directory testSessionDir = path.join(os.tmpdir(), `error-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 } }) /** * Session save failure tests */ describe('Session save failure handling', () => { it('should not throw error when session save fails with invalid session ID', async () => { const manager = new SessionManager(sessionConfig) const invalidSessionId = '../../../etc/passwd' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } // Should not throw error 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 error was logged expect(consoleErrorSpy).toHaveBeenCalled() const errorCalls = consoleErrorSpy.mock.calls const hasSaveError = errorCalls.some((call) => JSON.stringify(call).includes('Failed to save session') ) expect(hasSaveError).toBe(true) consoleErrorSpy.mockRestore() }) it('should handle file system write errors gracefully', async () => { // Create a read-only directory to simulate write failure const readOnlyDir = path.join(os.tmpdir(), `readonly-sessions-${Date.now()}`) await fs.mkdir(readOnlyDir, { mode: 0o555 }) const readOnlyConfig: SessionConfig = { enabled: true, sessionDir: readOnlyDir, retentionDays: 7, } const manager = new SessionManager(readOnlyConfig) const sessionId = 'test-readonly' const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } // Should not throw error even when directory is read-only await expect(manager.saveSession(sessionId, request, response)).resolves.toBeUndefined() // Cleanup: restore permissions and delete directory await fs.chmod(readOnlyDir, 0o755) await fs.rm(readOnlyDir, { recursive: true, force: true }) }) it('should continue main flow even 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, } // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Save should complete without throwing await manager.saveSession(invalidSessionId, request, response) // Main flow continues - we can still use the manager const validSessionId = 'valid-session' await manager.saveSession(validSessionId, request, response) // Verify valid session was saved const loadedSession = await manager.loadSession(validSessionId, 'rule-advisor') expect(loadedSession).not.toBeNull() consoleErrorSpy.mockRestore() }) }) /** * Session load failure tests */ describe('Session load failure handling', () => { it('should return null when session file does not exist', async () => { const manager = new SessionManager(sessionConfig) const nonExistentSessionId = 'non-existent-session' const result = await manager.loadSession(nonExistentSessionId, 'rule-advisor') expect(result).toBeNull() }) it('should return null when JSON parse fails', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'invalid-json-session' // 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 result = await manager.loadSession(sessionId, 'rule-advisor') expect(result).toBeNull() }) it('should log error when session load fails', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'load-error-session' // Create a file with invalid JSON const fileName = `${sessionId}_rule-advisor.json` const filePath = path.join(testSessionDir, fileName) await fs.writeFile(filePath, 'invalid json', 'utf-8') // Spy on console.error const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) await manager.loadSession(sessionId, 'rule-advisor') // Verify error was logged expect(consoleErrorSpy).toHaveBeenCalled() const errorCalls = consoleErrorSpy.mock.calls const hasLoadError = errorCalls.some((call) => JSON.stringify(call).includes('Failed to load session') ) expect(hasLoadError).toBe(true) consoleErrorSpy.mockRestore() }) it('should handle file system read errors gracefully', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'read-error-session' // Create a session file const request = { agent: 'rule-advisor', prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Find the created file const files = await fs.readdir(testSessionDir) const sessionFile = files.find((f) => f.startsWith(sessionId)) expect(sessionFile).toBeDefined() if (sessionFile) { const filePath = path.join(testSessionDir, sessionFile) // Make file unreadable await fs.chmod(filePath, 0o000) // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Should return null without throwing const result = await manager.loadSession(sessionId, 'rule-advisor') expect(result).toBeNull() consoleErrorSpy.mockRestore() // Restore permissions for cleanup await fs.chmod(filePath, 0o644) } }) it('should return null for invalid session ID without throwing', async () => { const manager = new SessionManager(sessionConfig) const invalidSessionId = '../../../etc/passwd' // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const result = await manager.loadSession(invalidSessionId, 'rule-advisor') expect(result).toBeNull() consoleErrorSpy.mockRestore() }) }) /** * File system error tests */ describe('File system error handling', () => { it('should handle directory read errors in cleanup', async () => { // Create a directory and then make it inaccessible const inaccessibleDir = path.join(os.tmpdir(), `inaccessible-sessions-${Date.now()}`) await fs.mkdir(inaccessibleDir, { mode: 0o755 }) const inaccessibleConfig: SessionConfig = { enabled: true, sessionDir: inaccessibleDir, retentionDays: 7, } const manager = new SessionManager(inaccessibleConfig) // Make directory inaccessible await fs.chmod(inaccessibleDir, 0o000) // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Cleanup should not throw error await expect(manager.cleanupOldSessions()).resolves.toBeUndefined() consoleErrorSpy.mockRestore() // Restore permissions and cleanup await fs.chmod(inaccessibleDir, 0o755) await fs.rm(inaccessibleDir, { recursive: true, force: true }) }) it('should continue cleanup even when individual file deletion fails', async () => { const manager = new SessionManager(sessionConfig) // Create multiple old files const fileCount = 5 const filePaths: string[] = [] for (let i = 0; i < fileCount; i++) { const fileName = `old-session-${i}_rule-advisor.json` const filePath = path.join(testSessionDir, fileName) await fs.writeFile(filePath, JSON.stringify({ test: 'data' }), 'utf-8') filePaths.push(filePath) // Set file modification time to 8 days ago const eightDaysAgo = new Date() eightDaysAgo.setDate(eightDaysAgo.getDate() - 8) await fs.utimes(filePath, eightDaysAgo, eightDaysAgo) } // Make the middle file read-only to simulate deletion failure if (filePaths[2]) { await fs.chmod(filePaths[2], 0o444) } // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Cleanup should not throw error await manager.cleanupOldSessions() consoleErrorSpy.mockRestore() // Restore permissions for cleanup for (const filePath of filePaths) { try { await fs.chmod(filePath, 0o644) } catch { // Ignore if file was already deleted } } }) it('should handle stat errors during cleanup', 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 inaccessible (can't stat) await fs.chmod(filePath, 0o000) // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Cleanup should not throw error await expect(manager.cleanupOldSessions()).resolves.toBeUndefined() consoleErrorSpy.mockRestore() // Restore permissions for cleanup try { await fs.chmod(filePath, 0o644) } catch { // Ignore if file was already deleted } }) }) /** * Validation error tests */ describe('Validation error handling', () => { it('should reject empty session ID', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('')).toThrow('Invalid session ID') }) it('should reject session ID with special characters', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('session@123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('session/123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('session\\123')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('session 123')).toThrow('Invalid session ID') }) it('should reject session ID 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 ID with path separators', () => { const manager = new SessionManager(sessionConfig) expect(() => manager.validateSessionId('session/id')).toThrow('Invalid session ID') expect(() => manager.validateSessionId('session\\id')).toThrow('Invalid session ID') }) }) /** * Recovery and resilience tests */ describe('Recovery and resilience', () => { it('should recover from save failure and continue with valid operations', async () => { const manager = new SessionManager(sessionConfig) // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // First, try to save with invalid session ID const invalidSessionId = '../invalid' const request1 = { agent: 'rule-advisor', prompt: 'Invalid prompt', } const response1 = { stdout: 'Invalid output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(invalidSessionId, request1, response1) // Then, save with valid session ID const validSessionId = 'valid-recovery-test' const request2 = { agent: 'rule-advisor', prompt: 'Valid prompt', } const response2 = { stdout: 'Valid output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(validSessionId, request2, response2) // Verify valid session was saved const loadedSession = await manager.loadSession(validSessionId, 'rule-advisor') expect(loadedSession).not.toBeNull() expect(loadedSession?.sessionId).toBe(validSessionId) consoleErrorSpy.mockRestore() }) it('should handle multiple concurrent error scenarios', async () => { const manager = new SessionManager(sessionConfig) // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Try multiple invalid operations concurrently const promises = [ manager.saveSession( '../invalid1', { agent: 'test', prompt: 'test' }, { stdout: '', stderr: '', exitCode: 0, executionTime: 0 } ), manager.saveSession( '../invalid2', { agent: 'test', prompt: 'test' }, { stdout: '', stderr: '', exitCode: 0, executionTime: 0 } ), manager.loadSession('non-existent-1', 'rule-advisor'), manager.loadSession('non-existent-2', 'rule-advisor'), ] // All should complete without throwing await expect(Promise.all(promises)).resolves.toBeDefined() consoleErrorSpy.mockRestore() }) it('should maintain data integrity after error recovery', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'integrity-test' // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) // Save first entry const request1 = { agent: 'rule-advisor', prompt: 'First prompt', } const response1 = { stdout: 'First output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request1, response1) // Try to save with invalid ID (should fail but not affect valid session) await manager.saveSession( '../invalid', { agent: 'test', prompt: 'test' }, { stdout: '', stderr: '', exitCode: 0, executionTime: 0 } ) // Save second entry to the valid session const request2 = { agent: 'rule-advisor', prompt: 'Second prompt', } const response2 = { stdout: 'Second output', stderr: '', exitCode: 0, executionTime: 200, } await manager.saveSession(sessionId, request2, response2) // Verify data integrity const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession).not.toBeNull() expect(loadedSession?.history.length).toBeGreaterThanOrEqual(2) consoleErrorSpy.mockRestore() }) }) })

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