Skip to main content
Glama
story6-acceptance-criteria.test.ts12.1 kB
/** * Story 6: Comprehensive Response Collection and Output - Acceptance Criteria Tests * * Tests validate the acceptance criteria for Story 6: * Given I have executed both pre and post WebSocket phases * When all commands complete or timeout occurs * Then it should return concatenated WebSocket responses verbatim * And preserve exact formatting including CRLF line endings * And include both history replay and real-time messages * And provide clear separation between message phases * And handle timeout scenarios gracefully (10-second limit) * And clean up all resources (server, WebSocket, etc.) */ import { ComprehensiveResponseCollector } from './comprehensive-response-collector'; import WebSocket from 'ws'; describe('Story 6: Comprehensive Response Collection - Acceptance Criteria', () => { let collector: ComprehensiveResponseCollector; beforeEach(() => { collector = new ComprehensiveResponseCollector(); }); afterEach(async () => { if (collector) { await collector.cleanup(); } }); describe('Acceptance Criteria: Return concatenated WebSocket responses verbatim', () => { it('should concatenate WebSocket responses verbatim preserving exact formatting', async () => { // Arrange: Create mock components that return predictable responses const mockServerManager = { start: jest.fn().mockResolvedValue(undefined), stop: jest.fn().mockResolvedValue(undefined), isRunning: jest.fn().mockReturnValue(true), getRawProcess: jest.fn().mockReturnValue({ stdin: null, stdout: null }) } as any; const mockMcpClient = { isConnected: jest.fn().mockReturnValue(true), callTool: jest.fn().mockResolvedValue({ success: true }), disconnect: jest.fn().mockResolvedValue(undefined) } as any; const mockPreWebSocketExecutor = { executeCommands: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue(undefined) } as any; const mockConnectionDiscovery = { discoverWebSocketUrl: jest.fn().mockResolvedValue('ws://localhost:8083/ws/session/test-session'), establishConnection: jest.fn().mockResolvedValue({ readyState: WebSocket.OPEN, close: jest.fn(), terminate: jest.fn(), removeAllListeners: jest.fn() } as any) } as any; const mockHistoryCapture = { captureInitialHistory: jest.fn().mockResolvedValue(undefined), waitForHistoryReplayComplete: jest.fn().mockResolvedValue(undefined), getHistoryMessages: jest.fn().mockReturnValue([ { timestamp: 1000, data: 'History message 1\r\n', isHistoryReplay: true, sequenceNumber: 1 }, { timestamp: 1001, data: 'History message 2\r\n', isHistoryReplay: true, sequenceNumber: 2 } ]), getRealTimeMessages: jest.fn().mockReturnValue([ { timestamp: 2000, data: 'Real-time message 1\r\n', isHistoryReplay: false, sequenceNumber: 3 }, { timestamp: 2001, data: 'Real-time message 2\r\n', isHistoryReplay: false, sequenceNumber: 4 } ]), cleanup: jest.fn().mockResolvedValue(undefined) } as any; const mockPostWebSocketExecutor = { executeCommands: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue(undefined) } as any; // Set up collector with mocked components collector.setServerManager(mockServerManager); collector.setMcpClient(mockMcpClient); collector.setPreWebSocketExecutor(mockPreWebSocketExecutor); collector.setConnectionDiscovery(mockConnectionDiscovery); collector.setHistoryCapture(mockHistoryCapture); collector.setPostWebSocketExecutor(mockPostWebSocketExecutor); // Act: Execute comprehensive workflow const result = await collector.executeComprehensiveWorkflow(); // Assert: Verify concatenated responses are verbatim and preserve formatting expect(result.success).toBe(true); expect(result.concatenatedResponses).toBe('History message 1\r\nHistory message 2\r\nReal-time message 1\r\nReal-time message 2\r\n'); // Verify exact formatting preservation (CRLF line endings) expect(result.concatenatedResponses.includes('\r\n')).toBe(true); expect(result.concatenatedResponses.includes('\n\r')).toBe(false); // No line ending corruption }); }); describe('Acceptance Criteria: Include both history replay and real-time messages', () => { it('should include both history replay and real-time messages with clear separation', async () => { // Arrange: Mock components with distinct history and real-time messages const mockComponents = createMockComponents(); mockComponents.historyCapture.getHistoryMessages.mockReturnValue([ { timestamp: 1000, data: 'HISTORY: command output\r\n', isHistoryReplay: true, sequenceNumber: 1 } ]); mockComponents.historyCapture.getRealTimeMessages.mockReturnValue([ { timestamp: 2000, data: 'REALTIME: fresh output\r\n', isHistoryReplay: false, sequenceNumber: 2 } ]); setupCollectorWithMocks(collector, mockComponents); // Act: Execute workflow const result = await collector.executeComprehensiveWorkflow(); // Assert: Verify both types of messages are included and separated expect(result.success).toBe(true); expect(result.phaseBreakdown!.historyMessageCount).toBe(1); expect(result.phaseBreakdown!.realTimeMessageCount).toBe(1); expect(result.phaseBreakdown!.historyReplayMessages).toEqual(['HISTORY: command output\r\n']); expect(result.phaseBreakdown!.realTimeMessages).toEqual(['REALTIME: fresh output\r\n']); expect(result.concatenatedResponses).toBe('HISTORY: command output\r\nREALTIME: fresh output\r\n'); }); }); describe('Acceptance Criteria: Handle timeout scenarios gracefully', () => { it('should handle workflow timeout gracefully with configurable limit', async () => { // Arrange: Create collector with short timeout and slow mock components const timeoutCollector = new ComprehensiveResponseCollector({ workflowTimeout: 100 }); const mockComponents = createMockComponents(); // Make the entire workflow take longer than timeout // Server is running but pre-WebSocket commands take too long mockComponents.serverManager.isRunning.mockReturnValue(false); // Force server start mockComponents.serverManager.start.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(undefined), 50)) ); mockComponents.preWebSocketExecutor.executeCommands.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve([]), 120)) // This will timeout ); setupCollectorWithMocks(timeoutCollector, mockComponents); // Act: Execute workflow const result = await timeoutCollector.executeComprehensiveWorkflow(); // Assert: Verify timeout is handled gracefully expect(result.success).toBe(false); expect(result.error).toContain('Workflow timeout'); expect(result.totalExecutionTime).toBeGreaterThanOrEqual(100); await timeoutCollector.cleanup(); }); it('should use default 10-second timeout when not specified', async () => { // Arrange: Create collector with default configuration const defaultCollector = new ComprehensiveResponseCollector(); const config = defaultCollector.getConfig(); // Assert: Verify default timeout is 10 seconds (10000ms) expect(config.workflowTimeout).toBe(10000); await defaultCollector.cleanup(); }); }); describe('Acceptance Criteria: Clean up all resources', () => { it('should clean up all resources (server, WebSocket, etc.)', async () => { // Arrange: Mock components with cleanup tracking const mockComponents = createMockComponents(); setupCollectorWithMocks(collector, mockComponents); // Act: Execute workflow and then cleanup await collector.executeComprehensiveWorkflow(); await collector.cleanup(); // Assert: Verify all cleanup methods were called expect(mockComponents.serverManager.stop).toHaveBeenCalled(); expect(mockComponents.mcpClient.disconnect).toHaveBeenCalled(); expect(mockComponents.preWebSocketExecutor.cleanup).toHaveBeenCalled(); expect(mockComponents.historyCapture.cleanup).toHaveBeenCalled(); expect(mockComponents.postWebSocketExecutor.cleanup).toHaveBeenCalled(); }); }); describe('Acceptance Criteria: Preserve exact formatting including CRLF line endings', () => { it('should preserve CRLF line endings required for xterm.js terminal display', async () => { // Arrange: Mock components with various line ending formats const mockComponents = createMockComponents(); mockComponents.historyCapture.getHistoryMessages.mockReturnValue([ { timestamp: 1000, data: 'Line 1\r\n', isHistoryReplay: true, sequenceNumber: 1 }, { timestamp: 1001, data: 'Line 2\r\n', isHistoryReplay: true, sequenceNumber: 2 } ]); mockComponents.historyCapture.getRealTimeMessages.mockReturnValue([ { timestamp: 2000, data: 'Real line 1\r\n', isHistoryReplay: false, sequenceNumber: 3 } ]); setupCollectorWithMocks(collector, mockComponents); // Act: Execute workflow const result = await collector.executeComprehensiveWorkflow(); // Assert: Verify CRLF preservation (critical for terminal display) expect(result.success).toBe(true); expect(result.concatenatedResponses).toBe('Line 1\r\nLine 2\r\nReal line 1\r\n'); // Verify all CRLF sequences are preserved const crlfCount = (result.concatenatedResponses.match(/\r\n/g) || []).length; expect(crlfCount).toBe(3); // Verify no line ending corruption expect(result.concatenatedResponses.includes('\n\r')).toBe(false); }); }); // Helper functions function createMockComponents() { return { serverManager: { start: jest.fn().mockResolvedValue(undefined), stop: jest.fn().mockResolvedValue(undefined), isRunning: jest.fn().mockReturnValue(true), getRawProcess: jest.fn().mockReturnValue({ stdin: null, stdout: null }) } as any, mcpClient: { isConnected: jest.fn().mockReturnValue(true), callTool: jest.fn().mockResolvedValue({ success: true }), disconnect: jest.fn().mockResolvedValue(undefined) } as any, preWebSocketExecutor: { executeCommands: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue(undefined) } as any, connectionDiscovery: { discoverWebSocketUrl: jest.fn().mockResolvedValue('ws://localhost:8083/ws/session/test-session'), establishConnection: jest.fn().mockResolvedValue({ readyState: WebSocket.OPEN, close: jest.fn(), terminate: jest.fn(), removeAllListeners: jest.fn() } as any) } as any, historyCapture: { captureInitialHistory: jest.fn().mockResolvedValue(undefined), waitForHistoryReplayComplete: jest.fn().mockResolvedValue(undefined), getHistoryMessages: jest.fn().mockReturnValue([]), getRealTimeMessages: jest.fn().mockReturnValue([]), cleanup: jest.fn().mockResolvedValue(undefined) } as any, postWebSocketExecutor: { executeCommands: jest.fn().mockResolvedValue([]), cleanup: jest.fn().mockResolvedValue(undefined) } as any }; } function setupCollectorWithMocks(collector: ComprehensiveResponseCollector, mocks: any) { collector.setServerManager(mocks.serverManager); collector.setMcpClient(mocks.mcpClient); collector.setPreWebSocketExecutor(mocks.preWebSocketExecutor); collector.setConnectionDiscovery(mocks.connectionDiscovery); collector.setHistoryCapture(mocks.historyCapture); collector.setPostWebSocketExecutor(mocks.postWebSocketExecutor); } });

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/LightspeedDMS/ssh-mcp'

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