Skip to main content
Glama
session-acceptance.test.ts22.5 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 } from 'vitest' import { formatSessionHistory } from '../../session/SessionHistoryFormatter' import { SessionManager } from '../../session/SessionManager' import type { SessionConfig } from '../../types/SessionData' /** * Acceptance tests for session management feature. * * These tests verify that all 10 acceptance criteria from the Design Doc are met: * 1. Environment variable configuration * 2. Session save functionality * 3. Session load functionality * 4. Session history formatting * 5. Token reduction with Markdown (30%+) * 6. File naming convention * 7. Cleanup functionality * 8. Error isolation * 9. Backward compatibility * 10. Debuggability (JSON format) */ describe('Session Management - Acceptance Tests', () => { let testSessionDir: string let sessionConfig: SessionConfig beforeEach(async () => { // Create a temporary test directory testSessionDir = path.join(os.tmpdir(), `acceptance-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 } }) /** * Acceptance Criterion 1: Environment variable configuration * SESSION_ENABLED=true enables session management */ describe('AC1: Environment variable configuration', () => { it('should enable session management when SESSION_ENABLED=true', () => { const enabledConfig: SessionConfig = { enabled: true, sessionDir: testSessionDir, retentionDays: 7, } const manager = new SessionManager(enabledConfig) expect(manager).toBeInstanceOf(SessionManager) }) it('should use custom SESSION_DIR when specified', async () => { const customDir = path.join(os.tmpdir(), `custom-sessions-${Date.now()}`) const customConfig: SessionConfig = { enabled: true, sessionDir: customDir, retentionDays: 7, } const manager = new SessionManager(customConfig) expect(manager).toBeInstanceOf(SessionManager) // Verify directory was created const dirExists = await fs.stat(customDir) expect(dirExists.isDirectory()).toBe(true) // Cleanup await fs.rm(customDir, { recursive: true, force: true }) }) it('should use custom SESSION_RETENTION_DAYS when specified', () => { const customRetentionConfig: SessionConfig = { enabled: true, sessionDir: testSessionDir, retentionDays: 14, } const manager = new SessionManager(customRetentionConfig) expect(manager).toBeInstanceOf(SessionManager) }) }) /** * Acceptance Criterion 2: Session save functionality * run_agent tool saves request-response when session_id is specified */ describe('AC2: Session save functionality', () => { it('should save session data with session_id specified', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac2-test-session' const request = { agent: 'rule-advisor', prompt: 'Test prompt for AC2', cwd: '/test/dir', } const response = { stdout: 'Test output for AC2', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Verify file was created const files = await fs.readdir(testSessionDir) expect(files.length).toBeGreaterThan(0) // Verify file contains correct data const sessionFile = files.find((f) => f.startsWith(sessionId)) expect(sessionFile).toBeDefined() if (sessionFile) { const filePath = path.join(testSessionDir, sessionFile) 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) } }) }) /** * Acceptance Criterion 3: Session load functionality * Existing session history is passed to subagent when session_id is reused */ describe('AC3: Session load functionality', () => { it('should load existing session history when session_id is reused', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac3-test-session' const request1 = { agent: 'rule-advisor', prompt: 'First prompt', cwd: '/test/dir', } const response1 = { stdout: 'First output', stderr: '', exitCode: 0, executionTime: 100, } // Save first session await manager.saveSession(sessionId, request1, response1) // Load session const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') // Verify loaded data expect(loadedSession).not.toBeNull() expect(loadedSession?.sessionId).toBe(sessionId) expect(loadedSession?.history).toHaveLength(1) expect(loadedSession?.history[0].request).toEqual(request1) expect(loadedSession?.history[0].response).toEqual(response1) }) it('should load session with multiple history entries', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac3-multi-history' 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 two sessions await manager.saveSession(sessionId, request1, response1) await manager.saveSession(sessionId, request2, response2) // Load session const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') // Verify both entries are in history expect(loadedSession).not.toBeNull() expect(loadedSession?.history.length).toBeGreaterThanOrEqual(2) }) }) /** * Acceptance Criterion 4: Session history formatting * Session information passed to subagent is converted to Markdown format */ describe('AC4: Session history formatting', () => { it('should convert session data to Markdown format', () => { const sessionData = { sessionId: 'ac4-test', agentType: 'rule-advisor', history: [ { timestamp: new Date('2025-01-21T12:00:00Z'), request: { agent: 'rule-advisor', prompt: 'Test prompt', }, response: { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, }, }, ], createdAt: new Date('2025-01-21T12:00:00Z'), lastUpdatedAt: new Date('2025-01-21T12:00:00Z'), } const markdown = formatSessionHistory(sessionData) // Verify Markdown format structure expect(markdown).toContain('# Session History: rule-advisor') expect(markdown).toContain('## 1. User Request') expect(markdown).toContain('Test prompt') expect(markdown).toContain('## 1. Agent Response') expect(markdown).toContain('Test output') // Verify metadata is NOT included (token optimization) expect(markdown).not.toContain('ac4-test') expect(markdown).not.toContain('2025-01-21T12:00:00.000Z') expect(markdown).not.toContain('exitCode') }) it('should preserve conversation flow across multiple interactions', () => { const sessionData = { sessionId: 'ac4-multi-test', agentType: 'rule-advisor', history: [ { timestamp: new Date('2025-01-21T12:00:00Z'), request: { agent: 'rule-advisor', prompt: 'First question', }, response: { stdout: 'First answer', stderr: '', exitCode: 0, executionTime: 100, }, }, { timestamp: new Date('2025-01-21T12:05:00Z'), request: { agent: 'rule-advisor', prompt: 'Second question', }, response: { stdout: 'Second answer', stderr: '', exitCode: 0, executionTime: 200, }, }, ], createdAt: new Date('2025-01-21T12:00:00Z'), lastUpdatedAt: new Date('2025-01-21T12:05:00Z'), } const markdown = formatSessionHistory(sessionData) // Verify all interactions are preserved expect(markdown).toContain('## 1. User Request') expect(markdown).toContain('First question') expect(markdown).toContain('## 1. Agent Response') expect(markdown).toContain('First answer') expect(markdown).toContain('## 2. User Request') expect(markdown).toContain('Second question') expect(markdown).toContain('## 2. Agent Response') expect(markdown).toContain('Second answer') }) }) /** * Acceptance Criterion 5: Token reduction with Markdown * Markdown format provides significant token reduction (52%+) by eliminating metadata */ describe('AC5: Token reduction with Markdown', () => { it('should achieve 30% or more token reduction with Markdown format', () => { // Markdown format eliminates unnecessary metadata (sessionId, timestamps, etc.) // while preserving essential conversation content const sessionData = { sessionId: 'token-reduction-test-session-id', agentType: 'rule-advisor', history: [ { timestamp: new Date('2025-01-21T12:00:00Z'), request: { agent: 'rule-advisor', prompt: 'This is a test prompt with some meaningful content', }, response: { stdout: 'This is a test output with some meaningful content', stderr: '', exitCode: 0, executionTime: 150, }, }, { timestamp: new Date('2025-01-21T12:05:00Z'), request: { agent: 'rule-advisor', prompt: 'Another test prompt to add more data', }, response: { stdout: 'Another test output', stderr: '', exitCode: 0, executionTime: 200, }, }, ], createdAt: new Date('2025-01-21T12:00:00Z'), lastUpdatedAt: new Date('2025-01-21T12:05:00Z'), } // Convert to JSON string const jsonStr = JSON.stringify(sessionData) // Convert to Markdown string const markdownStr = formatSessionHistory(sessionData) // Verify conversion succeeded and produced valid output expect(markdownStr).toBeDefined() expect(typeof markdownStr).toBe('string') expect(markdownStr.length).toBeGreaterThan(0) // Verify Markdown format is human-readable (contains newlines for structure) expect(markdownStr).toContain('\n') expect(markdownStr).toContain('# Session History') // Calculate token reduction const jsonLength = jsonStr.length const markdownLength = markdownStr.length const reductionRate = ((jsonLength - markdownLength) / jsonLength) * 100 // Verify 30% or more reduction expect(reductionRate).toBeGreaterThanOrEqual(30) // Log for visibility console.log(`Token reduction rate: ${reductionRate.toFixed(2)}%`) console.log(`JSON length: ${jsonLength}, Markdown length: ${markdownLength}`) }) }) /** * Acceptance Criterion 6: File naming convention * Files are saved in [session_id]_[agent_type]_[timestamp].json format */ describe('AC6: File naming convention', () => { it('should save files with correct naming convention', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac6-naming-test' const agentType = 'rule-advisor' const request = { agent: agentType, prompt: 'Test prompt', } const response = { stdout: 'Test output', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Verify file naming convention const files = await fs.readdir(testSessionDir) const sessionFile = files.find((f) => f.startsWith(sessionId)) expect(sessionFile).toBeDefined() // Format: [session_id]_[agent_type].json expect(sessionFile).toBe(`${sessionId}_${agentType}.json`) }) }) /** * Acceptance Criterion 7: Cleanup functionality * Session files older than 7 days are deleted on request */ describe('AC7: Cleanup functionality', () => { it('should delete session files older than retention days', async () => { const manager = new SessionManager(sessionConfig) // Create an old file (8 days ago) const oldFileName = `old-session_rule-advisor_${Date.now()}.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 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_${Date.now()}.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 and recent file remains const files = await fs.readdir(testSessionDir) expect(files).not.toContain(oldFileName) expect(files).toContain(recentFileName) }) }) /** * Acceptance Criterion 8: Error isolation * run_agent tool execution succeeds even when session save fails */ describe('AC8: Error isolation', () => { 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 return null when session load fails without throwing error', async () => { const manager = new SessionManager(sessionConfig) const nonExistentSessionId = 'non-existent-session' // Should return null without throwing error const result = await manager.loadSession(nonExistentSessionId, 'rule-advisor') expect(result).toBeNull() }) it('should not throw error when cleanup encounters permission errors', async () => { const manager = new SessionManager(sessionConfig) // Create a file const fileName = `test-session_rule-advisor_${Date.now()}.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) // Should not throw error even if deletion fails await expect(manager.cleanupOldSessions()).resolves.toBeUndefined() // Restore permissions for cleanup try { await fs.chmod(filePath, 0o644) } catch { // Ignore if file was already deleted } }) }) /** * Acceptance Criterion 9: Backward compatibility * Existing behavior (without session management) is maintained when session_id is not specified */ describe('AC9: Backward compatibility', () => { it('should not create session files when session_id is not specified', async () => { const manager = new SessionManager(sessionConfig) // Simulate run_agent without session_id by not calling saveSession // In the actual implementation, RunAgentTool should skip session save when session_id is not provided // Verify no files were created const files = await fs.readdir(testSessionDir) expect(files.length).toBe(0) }) it('should allow SessionManager to be created with disabled config', () => { const disabledConfig: SessionConfig = { enabled: false, sessionDir: testSessionDir, retentionDays: 7, } // Should still create manager even when disabled // This ensures backward compatibility at the configuration level const manager = new SessionManager(disabledConfig) expect(manager).toBeInstanceOf(SessionManager) }) }) /** * Acceptance Criterion 10: Debuggability (JSON format) * Session files are in JSON format with high readability */ describe('AC10: Debuggability (JSON format)', () => { it('should save session files in readable JSON format', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac10-debug-test' const request = { agent: 'rule-advisor', prompt: 'Test prompt for debugging', cwd: '/test/dir', } const response = { stdout: 'Test output for debugging', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request, response) // Read 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) const fileContent = await fs.readFile(filePath, 'utf-8') // Verify it's valid JSON expect(() => JSON.parse(fileContent)).not.toThrow() // Verify JSON is pretty-printed (contains newlines and indentation) expect(fileContent).toContain('\n') expect(fileContent).toContain(' ') // Verify all expected fields are present const sessionData = JSON.parse(fileContent) expect(sessionData).toHaveProperty('sessionId') expect(sessionData).toHaveProperty('agentType') expect(sessionData).toHaveProperty('history') expect(sessionData).toHaveProperty('createdAt') expect(sessionData).toHaveProperty('lastUpdatedAt') } }) it('should include complete request and response data for debugging', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'ac10-complete-data' const request = { agent: 'rule-advisor', prompt: 'Detailed test prompt', cwd: '/test/dir', env: { TEST_VAR: 'value' }, } const response = { stdout: 'Detailed test output', stderr: 'Warning message', exitCode: 0, executionTime: 150, } await manager.saveSession(sessionId, request, response) // Load and verify complete data const loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession).not.toBeNull() expect(loadedSession?.history[0].request).toEqual(request) expect(loadedSession?.history[0].response).toEqual(response) }) }) /** * Integration test: Complete workflow * Tests the entire session lifecycle from save to load to cleanup */ describe('Integration: Complete session workflow', () => { it('should handle complete session lifecycle', async () => { const manager = new SessionManager(sessionConfig) const sessionId = 'integration-test-session' // 1. Save first interaction const request1 = { agent: 'rule-advisor', prompt: 'First interaction', } const response1 = { stdout: 'First response', stderr: '', exitCode: 0, executionTime: 100, } await manager.saveSession(sessionId, request1, response1) // 2. Load session let loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession?.history).toHaveLength(1) // 3. Save second interaction (append to history) const request2 = { agent: 'rule-advisor', prompt: 'Second interaction', } const response2 = { stdout: 'Second response', stderr: '', exitCode: 0, executionTime: 200, } await manager.saveSession(sessionId, request2, response2) // 4. Load updated session loadedSession = await manager.loadSession(sessionId, 'rule-advisor') expect(loadedSession?.history.length).toBeGreaterThanOrEqual(2) // 5. Verify Markdown formatting works with loaded data if (loadedSession) { const markdown = formatSessionHistory(loadedSession) expect(markdown).toBeDefined() expect(markdown).toContain('# Session History') expect(markdown.length).toBeGreaterThan(0) } // 6. Cleanup (should not delete recent files) await manager.cleanupOldSessions() const files = await fs.readdir(testSessionDir) expect(files.length).toBeGreaterThan(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/shinpr/sub-agents-mcp'

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