Skip to main content
Glama
workflow-error-handler.test.ts19.3 kB
/** * Tests for WorkflowErrorHandler */ import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { WorkflowErrorHandler, WorkflowErrorType } from '../../src/utils/workflow-error-handler.js'; import { AppError, ErrorType } from '../../src/types/index.js'; import type { Logger, WorkflowDefinition } from '../../src/types/index.js'; import { MCPErrorCodes } from '../../src/utils/errors.js'; // Mock logger const mockLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), child: jest.fn().mockReturnThis() } as unknown as Logger; describe('WorkflowErrorHandler', () => { let errorHandler: WorkflowErrorHandler; beforeEach(() => { jest.clearAllMocks(); errorHandler = new WorkflowErrorHandler(mockLogger); }); describe('handleDiscoveryError', () => { it('should handle discovery errors gracefully and return empty array', () => { const error = new AppError(ErrorType.API_ERROR, 'Discovery failed'); const context = { operation: 'test_discovery' }; const result = errorHandler.handleDiscoveryError(error, context); expect(result).toEqual([]); // Check that the error was logged (the exact format may vary) expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining('Workflow discovery failed'), expect.any(Object) ); }); it('should detect workflows-list-tool unavailable errors', () => { const error = new AppError( ErrorType.API_ERROR, 'workflows-list-tool not found', {}, 404 ); const result = errorHandler.handleDiscoveryError(error); expect(result).toEqual([]); expect(mockLogger.warn).toHaveBeenCalledWith( 'workflows-list-tool is unavailable. Server will continue with static tools only.', expect.objectContaining({ context: expect.objectContaining({ operation: 'workflow_discovery' }) }) ); }); it('should handle generic errors', () => { const error = new Error('Generic discovery error'); const result = errorHandler.handleDiscoveryError(error); expect(result).toEqual([]); expect(mockLogger.warn).toHaveBeenCalled(); }); it('should record error metrics', () => { const error = new AppError(ErrorType.API_ERROR, 'Discovery failed'); errorHandler.handleDiscoveryError(error); const metrics = errorHandler.getErrorMetrics(); expect(metrics).toHaveLength(1); expect(metrics[0]).toMatchObject({ errorType: WorkflowErrorType.DISCOVERY_FAILED, operation: 'workflow_discovery', severity: 'warning', isRetryable: false, isGracefullyHandled: true }); }); }); describe('handleValidationError', () => { it('should handle validation errors and return null', () => { const workflow = { id: 'test', name: 'invalid' }; const error = new AppError(ErrorType.VALIDATION_ERROR, 'Invalid schema'); const result = errorHandler.handleValidationError(workflow, error); expect(result).toBeNull(); expect(mockLogger.info).toHaveBeenCalledWith( 'Skipping invalid workflow definition', expect.objectContaining({ error: 'Invalid schema', workflowData: workflow, context: expect.objectContaining({ operation: 'workflow_validation' }) }) ); }); it('should sanitize workflow data in logs', () => { const workflow = { id: 'test', name: 'test', apiKey: 'secret-key', password: 'secret-password' }; const error = new Error('Validation failed'); errorHandler.handleValidationError(workflow, error); expect(mockLogger.info).toHaveBeenCalledWith( 'Skipping invalid workflow definition', expect.objectContaining({ workflowData: expect.not.objectContaining({ apiKey: 'secret-key', password: 'secret-password' }) }) ); }); it('should record validation error metrics', () => { const workflow = { id: 'test' }; const error = new AppError(ErrorType.VALIDATION_ERROR, 'Invalid'); errorHandler.handleValidationError(workflow, error); const metrics = errorHandler.getErrorMetrics(); expect(metrics).toHaveLength(1); expect(metrics[0]).toMatchObject({ errorType: WorkflowErrorType.VALIDATION_FAILED, severity: 'info', isGracefullyHandled: true }); }); }); describe('handleExecutionError', () => { it('should handle execution errors and return error result', () => { const error = new AppError(ErrorType.API_ERROR, 'Execution failed'); const workflowId = 'test-workflow'; const parameters = { param1: 'value1' }; const result = errorHandler.handleExecutionError(error, workflowId, parameters); expect(result).toMatchObject({ success: false, originalWorkflowId: workflowId, status: 'FAILED', error: expect.stringContaining('Execution failed'), metadata: expect.objectContaining({ errorType: WorkflowErrorType.EXECUTION_FAILED, isRetryable: expect.any(Boolean) }) }); }); it('should classify timeout errors correctly', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Request timeout'); const workflowId = 'test-workflow'; const result = errorHandler.handleExecutionError(error, workflowId); expect(result.metadata?.errorType).toBe(WorkflowErrorType.TIMEOUT); expect(result.error).toContain('timed out'); }); it('should classify cancelled errors correctly', () => { const error = new Error('Operation was cancelled'); const workflowId = 'test-workflow'; const result = errorHandler.handleExecutionError(error, workflowId); expect(result.metadata?.errorType).toBe(WorkflowErrorType.CANCELLED); }); it('should sanitize parameters in context', () => { const error = new Error('Execution failed'); const workflowId = 'test-workflow'; const parameters = { username: 'user', password: 'secret', apiKey: 'key123' }; errorHandler.handleExecutionError(error, workflowId, parameters); const metrics = errorHandler.getErrorMetrics(); expect(metrics[0].context?.parameters).toEqual({ username: 'user', password: '[REDACTED]', apiKey: '[REDACTED]' }); }); }); describe('handleStatusCheckError', () => { it('should handle status check errors and return error status', () => { const error = new AppError(ErrorType.API_ERROR, 'Status check failed'); const workflowId = 'test-workflow'; const workflowInstanceId = 'instance-123'; const result = errorHandler.handleStatusCheckError(error, workflowId, workflowInstanceId); expect(result).toMatchObject({ status: 'FAILED', workflow_id: workflowId, workflowInstanceId, error: expect.stringContaining('status') }); }); it('should record status check error metrics', () => { const error = new Error('Status check failed'); const workflowId = 'test-workflow'; const workflowInstanceId = 'instance-123'; errorHandler.handleStatusCheckError(error, workflowId, workflowInstanceId); const metrics = errorHandler.getErrorMetrics(); expect(metrics).toHaveLength(1); expect(metrics[0]).toMatchObject({ errorType: WorkflowErrorType.STATUS_CHECK_FAILED, operation: 'workflow_status_check', workflowId, severity: 'warning' }); }); }); describe('handleToolGenerationError', () => { it('should handle tool generation errors and return null', () => { const workflow: WorkflowDefinition = { id: 'test-workflow', name: 'Test Workflow', description: 'Test description', inputSchema: { type: 'object', properties: {}, required: [] } }; const error = new Error('Tool generation failed'); const result = errorHandler.handleToolGenerationError(error, workflow); expect(result).toBeNull(); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Failed to generate tool for workflow Test Workflow'), expect.objectContaining({ error: 'Tool generation failed' }) ); }); it('should record tool generation error metrics', () => { const workflow: WorkflowDefinition = { id: 'test-workflow', name: 'Test Workflow', description: 'Test description', inputSchema: { type: 'object', properties: {}, required: [] } }; const error = new Error('Tool generation failed'); errorHandler.handleToolGenerationError(error, workflow); const metrics = errorHandler.getErrorMetrics(); expect(metrics).toHaveLength(1); expect(metrics[0]).toMatchObject({ errorType: WorkflowErrorType.TOOL_GENERATION_FAILED, operation: 'workflow_tool_generation', workflowId: 'test-workflow', severity: 'error' }); }); }); describe('createWorkflowToolErrorResponse', () => { it('should create MCP-formatted error response', () => { const error = new AppError(ErrorType.TOOL_ERROR, 'Tool execution failed'); const toolName = 'test-tool'; const parameters = { param1: 'value1' }; const result = errorHandler.createWorkflowToolErrorResponse(error, toolName, parameters); expect(result).toMatchObject({ code: MCPErrorCodes.TOOL_ERROR, message: expect.stringContaining('Tool execution failed'), data: expect.objectContaining({ toolName, details: expect.objectContaining({ isWorkflowError: true, workflowErrorType: expect.any(String), userFriendlyMessage: expect.any(String), isRetryable: expect.any(Boolean) }) }) }); }); it('should handle generic errors in tool execution', () => { const error = new Error('Generic tool error'); const toolName = 'test-tool'; const result = errorHandler.createWorkflowToolErrorResponse(error, toolName); expect(result.code).toBe(MCPErrorCodes.TOOL_ERROR); expect(result.data).toMatchObject({ toolName, details: expect.objectContaining({ isWorkflowError: true }) }); }); }); describe('isWorkflowsListToolUnavailable', () => { it('should detect API errors indicating tool unavailability', () => { const error = new AppError( ErrorType.API_ERROR, 'workflows-list-tool not found', {}, 404 ); const result = errorHandler.isWorkflowsListToolUnavailable(error); expect(result).toBe(true); }); it('should detect network errors as potential tool unavailability', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Connection refused'); const result = errorHandler.isWorkflowsListToolUnavailable(error); expect(result).toBe(true); }); it('should detect generic errors with specific messages', () => { const error = new Error('workflows-list-tool connection failed'); const result = errorHandler.isWorkflowsListToolUnavailable(error); expect(result).toBe(true); }); it('should not detect unrelated errors as tool unavailability', () => { const error = new AppError(ErrorType.VALIDATION_ERROR, 'Invalid input'); const result = errorHandler.isWorkflowsListToolUnavailable(error); expect(result).toBe(false); }); }); describe('error metrics and statistics', () => { it('should track error metrics', () => { const error1 = new Error('Error 1'); const error2 = new AppError(ErrorType.API_ERROR, 'Error 2'); errorHandler.handleDiscoveryError(error1); errorHandler.handleExecutionError(error2, 'workflow-1'); const metrics = errorHandler.getErrorMetrics(); expect(metrics).toHaveLength(2); }); it('should provide error statistics', () => { const error1 = new Error('Discovery error'); const error2 = new AppError(ErrorType.VALIDATION_ERROR, 'Validation error'); const error3 = new AppError(ErrorType.API_ERROR, 'Execution error'); errorHandler.handleDiscoveryError(error1); errorHandler.handleValidationError({}, error2); errorHandler.handleExecutionError(error3, 'workflow-1'); const stats = errorHandler.getErrorStatistics(); expect(stats.totalErrors).toBe(3); expect(stats.errorsByType[WorkflowErrorType.DISCOVERY_FAILED]).toBe(1); expect(stats.errorsByType[WorkflowErrorType.VALIDATION_FAILED]).toBe(1); expect(stats.errorsByType[WorkflowErrorType.EXECUTION_FAILED]).toBe(1); expect(stats.gracefullyHandledCount).toBe(3); }); it('should clear error metrics', () => { const error = new Error('Test error'); errorHandler.handleDiscoveryError(error); expect(errorHandler.getErrorMetrics()).toHaveLength(1); errorHandler.clearErrorMetrics(); expect(errorHandler.getErrorMetrics()).toHaveLength(0); }); it('should limit metrics history', () => { // Create error handler with small history limit for testing const smallErrorHandler = new (WorkflowErrorHandler as any)(mockLogger); smallErrorHandler.maxMetricsHistory = 2; // Add more errors than the limit for (let i = 0; i < 5; i++) { smallErrorHandler.handleDiscoveryError(new Error(`Error ${i}`)); } const metrics = smallErrorHandler.getErrorMetrics(); expect(metrics).toHaveLength(2); expect(metrics[0].message).toBe('Error 3'); expect(metrics[1].message).toBe('Error 4'); }); }); describe('error classification and severity', () => { it('should classify timeout errors correctly', () => { const timeoutError = new AppError(ErrorType.NETWORK_ERROR, 'Request timeout'); const result = errorHandler.handleExecutionError(timeoutError, 'workflow-1'); expect(result.metadata?.errorType).toBe(WorkflowErrorType.TIMEOUT); }); it('should classify API timeout errors correctly', () => { const apiTimeoutError = new AppError( ErrorType.API_ERROR, 'API timeout', {}, 408 ); const result = errorHandler.handleExecutionError(apiTimeoutError, 'workflow-1'); expect(result.metadata?.errorType).toBe(WorkflowErrorType.TIMEOUT); }); it('should assign appropriate severity levels', () => { const discoveryError = new Error('Discovery failed'); const validationError = new Error('Validation failed'); const executionError = new Error('Execution failed'); errorHandler.handleDiscoveryError(discoveryError); errorHandler.handleValidationError({}, validationError); errorHandler.handleExecutionError(executionError, 'workflow-1'); const metrics = errorHandler.getErrorMetrics(); expect(metrics.find(m => m.errorType === WorkflowErrorType.DISCOVERY_FAILED)?.severity).toBe('warning'); expect(metrics.find(m => m.errorType === WorkflowErrorType.VALIDATION_FAILED)?.severity).toBe('info'); expect(metrics.find(m => m.errorType === WorkflowErrorType.EXECUTION_FAILED)?.severity).toBe('error'); }); }); describe('user-friendly error messages', () => { it('should provide user-friendly messages for different error types', () => { const testCases = [ { error: new Error('workflows-list-tool not found'), expectedType: WorkflowErrorType.WORKFLOWS_LIST_TOOL_UNAVAILABLE, expectedMessage: 'Workflow discovery service is currently unavailable' }, { error: new Error('Request timeout'), expectedType: WorkflowErrorType.TIMEOUT, expectedMessage: 'Workflow execution timed out' }, { error: new Error('Operation cancelled'), expectedType: WorkflowErrorType.CANCELLED, expectedMessage: 'Workflow execution was cancelled' } ]; testCases.forEach(({ error, expectedMessage }) => { const result = errorHandler.handleExecutionError(error, 'workflow-1'); expect(result.error).toContain(expectedMessage.split(' ')[0]); }); }); }); describe('data sanitization', () => { it('should sanitize sensitive workflow data', () => { const workflow = { id: 'test', name: 'test', apiKey: 'secret-key', token: 'secret-token', password: 'secret-password', secret: 'secret-value', description: 'A'.repeat(300) // Long description }; errorHandler.handleValidationError(workflow, new Error('Test')); const logCall = (mockLogger.info as jest.MockedFunction<any>).mock.calls[0]; if (logCall && logCall[1] && logCall[1].workflowData) { const loggedWorkflow = logCall[1].workflowData; expect(loggedWorkflow).not.toHaveProperty('apiKey'); expect(loggedWorkflow).not.toHaveProperty('token'); expect(loggedWorkflow).not.toHaveProperty('password'); expect(loggedWorkflow).not.toHaveProperty('secret'); expect(loggedWorkflow.description).toHaveLength(203); // 200 + '...' } else { // If the log structure is different, just verify that logging occurred expect(mockLogger.info).toHaveBeenCalled(); } }); it('should sanitize sensitive parameters', () => { const parameters = { username: 'user', password: 'secret', apiKey: 'key123', authToken: 'token456', secretKey: 'secret789' }; errorHandler.handleExecutionError(new Error('Test'), 'workflow-1', parameters); const metrics = errorHandler.getErrorMetrics(); const sanitizedParams = metrics[0].context?.parameters; expect(sanitizedParams).toEqual({ username: 'user', password: '[REDACTED]', apiKey: '[REDACTED]', authToken: '[REDACTED]', secretKey: '[REDACTED]' }); }); }); describe('logging levels', () => { it('should use appropriate logging levels for different error types', () => { // Critical error (would be config error, but we'll simulate) const criticalError = new Error('Critical system error'); errorHandler.handleExecutionError(criticalError, 'workflow-1'); // Warning error const warningError = new Error('Discovery failed'); errorHandler.handleDiscoveryError(warningError); // Info error const infoError = new Error('Validation failed'); errorHandler.handleValidationError({}, infoError); // Verify appropriate log methods were called expect(mockLogger.error).toHaveBeenCalled(); expect(mockLogger.warn).toHaveBeenCalled(); expect(mockLogger.info).toHaveBeenCalled(); }); }); });

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/celeryhq/simplified-mcp-server'

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