Skip to main content
Glama
workflow-status-tool.test.ts24.7 kB
/** * Unit tests for Workflow Status Checking Tool */ import { createWorkflowStatusTool } from '../../src/tools/implementations/workflow-status-tool.js'; import type { APIClient, Logger, ToolDefinition } from '../../src/types/index.js'; import { AppError, ErrorType } from '../../src/types/index.js'; // Mock logger const mockLogger: Logger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; // Mock API client const mockApiClient: APIClient = { makeRequest: jest.fn(), get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn(), patch: jest.fn() }; describe('WorkflowStatusTool', () => { let tool: ToolDefinition; let mockGet: jest.MockedFunction<typeof mockApiClient.get>; beforeEach(() => { jest.clearAllMocks(); tool = createWorkflowStatusTool(mockLogger); mockGet = mockApiClient.get as jest.MockedFunction<typeof mockApiClient.get>; }); describe('Tool Definition', () => { it('should create a valid tool definition', () => { expect(tool.name).toBe('check-workflow-status'); expect(tool.description).toBe('Check the execution status of a running workflow via API calls'); expect(tool.category).toBe('workflow'); expect(tool.version).toBe('1.0.0'); expect(typeof tool.handler).toBe('function'); }); it('should have correct input schema', () => { expect(tool.inputSchema.type).toBe('object'); expect(tool.inputSchema.properties).toHaveProperty('workflowId'); expect(tool.inputSchema.properties).toHaveProperty('workflow_id'); expect(tool.inputSchema.properties).toHaveProperty('includeRawResponse'); expect(tool.inputSchema.required).toEqual(['workflowId', 'workflow_id']); }); it('should validate workflowId parameter schema', () => { const workflowIdSchema = tool.inputSchema.properties.workflowId; expect(workflowIdSchema.type).toBe('string'); expect(workflowIdSchema.minLength).toBe(1); expect(workflowIdSchema.maxLength).toBe(100); expect(workflowIdSchema.pattern).toBe('^[a-zA-Z0-9_-]+$'); }); it('should validate workflow_id parameter schema', () => { const workflowInstanceIdSchema = tool.inputSchema.properties.workflow_id; expect(workflowInstanceIdSchema.type).toBe('string'); expect(workflowInstanceIdSchema.pattern).toBe('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'); }); }); describe('Parameter Validation', () => { it('should reject missing workflowId', async () => { const params = { workflow_id: '12345678-1234-1234-1234-123456789abc' }; const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('VALIDATION_ERROR'); expect(response.error.message).toContain('workflowId is required'); }); it('should reject missing workflow_id', async () => { const params = { workflowId: '2724' }; const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('VALIDATION_ERROR'); expect(response.error.message).toContain('workflow_id is required'); }); it('should reject invalid workflowId format', async () => { const params = { workflowId: 'invalid@workflow#id', workflow_id: '12345678-1234-1234-1234-123456789abc' }; const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('VALIDATION_ERROR'); expect(response.error.message).toContain('alphanumeric characters, underscores, and hyphens'); }); it('should reject invalid workflow_id UUID format', async () => { const params = { workflowId: '2724', workflow_id: 'not-a-valid-uuid' }; const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('VALIDATION_ERROR'); expect(response.error.message).toContain('valid UUID format'); }); it('should accept valid parameters', async () => { const params = { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc' }; const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('COMPLETED'); expect(mockGet).toHaveBeenCalledWith( '/api/v1/service/workflows/2724/runs/12345678-1234-1234-1234-123456789abc/status', undefined, { headers: { 'Accept': 'application/json', 'Accept-Language': 'en' } } ); }); }); describe('API Integration', () => { const validParams = { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc' }; it('should make correct API call to status endpoint', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'RUNNING', start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); await tool.handler(validParams, mockApiClient); expect(mockGet).toHaveBeenCalledWith( '/api/v1/service/workflows/2724/runs/12345678-1234-1234-1234-123456789abc/status', undefined, { headers: { 'Accept': 'application/json', 'Accept-Language': 'en' } } ); }); it('should handle RUNNING status correctly', async () => { const createTime = 1641039781802; // 2022-01-01T12:23:01.802Z const updateTime = 1641039781850; // 2022-01-01T12:23:01.850Z const startTime = 1641039781802; // 2022-01-01T12:23:01.802Z const mockStatusResponse = { create_time: createTime, update_time: updateTime, status: 'RUNNING', start_time: startTime, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('RUNNING'); expect(response.workflowId).toBe('2724'); expect(response.workflowInstanceId).toBe('12345678-1234-1234-1234-123456789abc'); expect(response.createTime).toBe('2022-01-01T12:23:01.802Z'); expect(response.updateTime).toBe('2022-01-01T12:23:01.850Z'); expect(response.startTime).toBe('2022-01-01T12:23:01.802Z'); expect(response.input).toEqual({ target_domain: 'example.com' }); expect(response.output).toEqual({}); expect(response.endTime).toBeUndefined(); expect(response.executionDurationMs).toBeUndefined(); }); it('should handle COMPLETED status correctly', async () => { const createTime = 1641039781802; // 2022-01-01T12:23:01.802Z const updateTime = 1641039781904; // 2022-01-01T12:23:01.904Z const startTime = 1641039781802; // 2022-01-01T12:23:01.802Z const endTime = 1641039781904; // 2022-01-01T12:23:01.904Z const mockStatusResponse = { create_time: createTime, update_time: updateTime, status: 'COMPLETED', end_time: endTime, start_time: startTime, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success', data: 'processed' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('COMPLETED'); expect(response.endTime).toBe('2022-01-01T12:23:01.904Z'); expect(response.executionDurationMs).toBe(102); // endTime - startTime expect(response.output).toEqual({ result: 'success', data: 'processed' }); }); it('should handle FAILED status correctly', async () => { const createTime = 1641039781802; // 2022-01-01T12:23:01.802Z const updateTime = 1641039781904; // 2022-01-01T12:23:01.904Z const startTime = 1641039781802; // 2022-01-01T12:23:01.802Z const endTime = 1641039781904; // 2022-01-01T12:23:01.904Z const mockStatusResponse = { create_time: createTime, update_time: updateTime, status: 'FAILED', end_time: endTime, start_time: startTime, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { error: 'Domain not found' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('FAILED'); expect(response.error).toBe('Domain not found'); expect(response.endTime).toBe('2022-01-01T12:23:01.904Z'); expect(response.executionDurationMs).toBe(102); }); it('should handle CANCELLED status correctly', async () => { const createTime = 1641039781802; // 2022-01-01T12:23:01.802Z const updateTime = 1641039781904; // 2022-01-01T12:23:01.904Z const startTime = 1641039781802; // 2022-01-01T12:23:01.802Z const endTime = 1641039781904; // 2022-01-01T12:23:01.904Z const mockStatusResponse = { create_time: createTime, update_time: updateTime, status: 'CANCELLED', end_time: endTime, start_time: startTime, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('CANCELLED'); expect(response.endTime).toBe('2022-01-01T12:23:01.904Z'); }); it('should include raw response when requested', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const paramsWithRaw = { ...validParams, includeRawResponse: true }; const result = await tool.handler(paramsWithRaw, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.rawResponse).toEqual(mockStatusResponse); }); it('should not include raw response by default', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.rawResponse).toBeUndefined(); }); }); describe('Error Handling', () => { const validParams = { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc' }; it('should handle invalid API response format', async () => { mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: 'invalid response' }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_ERROR'); expect(response.error.message).toContain('Invalid status response format'); }); it('should handle missing required fields in response', async () => { const incompleteResponse = { create_time: 1641039781802, status: 'RUNNING' // Missing required fields: update_time, start_time, workflow_id, input, output }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: incompleteResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_ERROR'); expect(response.error.message).toContain('Missing required fields'); expect(response.error.details.missingFields).toContain('update_time'); expect(response.error.details.missingFields).toContain('start_time'); expect(response.error.details.missingFields).toContain('workflow_id'); expect(response.error.details.missingFields).toContain('input'); expect(response.error.details.missingFields).toContain('output'); }); it('should handle invalid status value', async () => { const invalidStatusResponse = { create_time: 1641039781802, update_time: 1641039781850, status: 'INVALID_STATUS', start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: invalidStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_ERROR'); expect(response.error.message).toContain('Invalid status value: INVALID_STATUS'); expect(response.error.message).toContain('RUNNING, COMPLETED, FAILED, CANCELLED'); }); it('should handle API unavailability (ECONNREFUSED)', async () => { const connectionError = new Error('connect ECONNREFUSED 127.0.0.1:3000'); connectionError.code = 'ECONNREFUSED'; mockGet.mockRejectedValue(connectionError); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_UNAVAILABLE'); expect(response.error.message).toBe('Workflow status checking API is not available'); expect(response.error.details.reason).toContain('not accessible'); }); it('should handle API unavailability (503 Service Unavailable)', async () => { const serviceError = new Error('Service Unavailable'); serviceError.status = 503; mockGet.mockRejectedValue(serviceError); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_UNAVAILABLE'); expect(response.error.message).toBe('Workflow status checking API is not available'); }); it('should handle API unavailability (502 Bad Gateway)', async () => { const gatewayError = new Error('Bad Gateway'); gatewayError.statusCode = 502; mockGet.mockRejectedValue(gatewayError); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_UNAVAILABLE'); }); it('should handle general API errors', async () => { const apiError = new Error('Internal Server Error'); mockGet.mockRejectedValue(apiError); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('API_ERROR'); expect(response.error.message).toContain('Failed to check workflow status'); }); it('should handle AppError instances', async () => { const appError = new AppError( ErrorType.AUTH_ERROR, 'Authentication failed', { reason: 'Invalid API key' }, 401 ); mockGet.mockRejectedValue(appError); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.error.type).toBe('AUTH_ERROR'); expect(response.error.message).toBe('Authentication failed'); expect(response.error.details.reason).toBe('Invalid API key'); expect(response.error.status).toBe(401); }); }); describe('Logging', () => { const validParams = { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc' }; it('should log status check start', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); await tool.handler(validParams, mockApiClient); expect(mockLogger.info).toHaveBeenCalledWith( 'Checking workflow status: workflowId=2724, workflow_id=12345678-1234-1234-1234-123456789abc' ); }); it('should log successful status check completion', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { result: 'success' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); await tool.handler(validParams, mockApiClient); expect(mockLogger.info).toHaveBeenCalledWith('Workflow status check completed: COMPLETED'); }); it('should log API endpoint being called', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'RUNNING', start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); await tool.handler(validParams, mockApiClient); expect(mockLogger.debug).toHaveBeenCalledWith( 'Making status check request to: /api/v1/service/workflows/2724/runs/12345678-1234-1234-1234-123456789abc/status' ); }); it('should log errors with context', async () => { const apiError = new Error('Network timeout'); mockGet.mockRejectedValue(apiError); await tool.handler(validParams, mockApiClient); expect(mockLogger.error).toHaveBeenCalledWith( 'Workflow status check failed: Network timeout', { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc', error: apiError } ); }); }); describe('Edge Cases', () => { const validParams = { workflowId: '2724', workflow_id: '12345678-1234-1234-1234-123456789abc' }; it('should handle FAILED status with no error in output', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'FAILED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} // No error field }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('FAILED'); expect(response.error).toBe('Workflow execution failed'); }); it('should handle FAILED status with error message in output', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'FAILED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: { message: 'Custom error message' } }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('FAILED'); expect(response.error).toBe('Custom error message'); }); it('should handle workflow with no end_time (still running)', async () => { const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781850, status: 'RUNNING', start_time: 1641039781802, workflow_id: '2724', input: { target_domain: 'example.com' }, output: {} // No end_time field }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(validParams, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('RUNNING'); expect(response.endTime).toBeUndefined(); expect(response.executionDurationMs).toBeUndefined(); }); it('should handle different workflowId formats', async () => { const testCases = [ '2724', 'workflow_123', 'my-workflow-name', 'WORKFLOW123', '123_workflow_456' ]; for (const workflowId of testCases) { const params = { workflowId, workflow_id: '12345678-1234-1234-1234-123456789abc' }; const mockStatusResponse = { create_time: 1641039781802, update_time: 1641039781904, status: 'COMPLETED', end_time: 1641039781904, start_time: 1641039781802, workflow_id: workflowId, input: {}, output: {} }; mockGet.mockResolvedValue({ status: 200, statusText: 'OK', data: mockStatusResponse }); const result = await tool.handler(params, mockApiClient); const response = JSON.parse(result.content[0].text); expect(response.status).toBe('COMPLETED'); expect(response.workflowId).toBe(workflowId); } }); }); });

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