Skip to main content
Glama
workflow-tool-manager.test.ts39.1 kB
/** * Unit tests for WorkflowToolManager */ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { WorkflowToolManager, type IWorkflowToolManager } from '../../src/services/workflow-tool-manager.js'; import { WorkflowDiscoveryService, type IWorkflowDiscoveryService } from '../../src/services/workflow-discovery.js'; import { WorkflowExecutionService } from '../../src/services/workflow-execution.js'; import { WorkflowStatusService } from '../../src/services/workflow-status.js'; import { ToolRegistry } from '../../src/tools/registry.js'; import type { APIClient, Logger, ServerConfig, WorkflowDefinition, WorkflowExecutionResult } from '../../src/types/index.js'; import { AppError, ErrorType } from '../../src/types/index.js'; // Mock the service dependencies jest.mock('../../src/services/workflow-discovery.js'); jest.mock('../../src/services/workflow-execution.js'); jest.mock('../../src/services/workflow-status.js'); describe('WorkflowToolManager', () => { let workflowToolManager: IWorkflowToolManager; let mockApiClient: APIClient; let mockLogger: Logger; let mockConfig: ServerConfig; let mockToolRegistry: ToolRegistry; let mockDiscoveryService: IWorkflowDiscoveryService; let mockExecutionService: WorkflowExecutionService; let mockStatusService: WorkflowStatusService; // Sample workflow definitions for testing const sampleWorkflows: WorkflowDefinition[] = [ { id: '1', name: 'test_workflow_1', description: 'Test workflow 1', category: 'test', version: '1.0.0', inputSchema: { type: 'object', properties: { param1: { type: 'string', description: 'Test parameter 1' } }, required: ['param1'] }, executionType: 'async' }, { id: '2', name: 'test_workflow_2', description: 'Test workflow 2', inputSchema: { type: 'object', properties: { param2: { type: 'number', description: 'Test parameter 2' } } } } ]; beforeEach(() => { // Create mock API client mockApiClient = { makeRequest: jest.fn(), get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn(), patch: jest.fn() }; // Create mock logger mockLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; // Create mock config mockConfig = { apiToken: 'test-token', apiBaseUrl: 'https://api.test.com', logLevel: 'info', timeout: 30000, retryAttempts: 3, retryDelay: 1000, workflowsEnabled: true, workflowDiscoveryInterval: 0, workflowExecutionTimeout: 300000, workflowMaxConcurrentExecutions: 10, workflowFilterPatterns: [], workflowStatusCheckInterval: 5000, workflowRetryAttempts: 3 }; // Create mock tool registry mockToolRegistry = new ToolRegistry(); jest.spyOn(mockToolRegistry, 'registerTool'); jest.spyOn(mockToolRegistry, 'unregisterTool'); jest.spyOn(mockToolRegistry, 'getTool'); // Create mock services mockDiscoveryService = { listWorkflows: jest.fn(), validateWorkflow: jest.fn(), isWorkflowsListToolAvailable: jest.fn(), testConnection: jest.fn(), clearCache: jest.fn(), getCacheStats: jest.fn() }; mockExecutionService = { executeWorkflow: jest.fn(), getExecutionStatus: jest.fn(), pollUntilComplete: jest.fn(), cancelExecution: jest.fn(), buildExecutionPayload: jest.fn(), buildExecutionEndpoint: jest.fn(), parseExecutionResponse: jest.fn(), setExecutionTimeout: jest.fn(), handleExecutionError: jest.fn(), getActiveExecutionsCount: jest.fn(), getActiveExecutionKeys: jest.fn(), cancelAllExecutions: jest.fn(), getConfig: jest.fn(), shutdown: jest.fn(), getPerformanceStats: jest.fn(), getPerformanceMonitor: jest.fn() }; mockStatusService = { checkStatus: jest.fn(), pollWithInterval: jest.fn(), stopPolling: jest.fn(), trackExecution: jest.fn(), cleanupCompletedExecutions: jest.fn(), getTrackedExecutionsCount: jest.fn(), getTrackedExecutionKeys: jest.fn(), getExecutionTracker: jest.fn(), shutdown: jest.fn(), getConfig: jest.fn() }; // Mock the service constructors (WorkflowDiscoveryService as any).mockImplementation(() => mockDiscoveryService); (WorkflowExecutionService as any).mockImplementation(() => mockExecutionService); (WorkflowStatusService as any).mockImplementation(() => mockStatusService); // Create WorkflowToolManager instance workflowToolManager = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); }); afterEach(() => { jest.clearAllMocks(); }); describe('constructor', () => { it('should create WorkflowToolManager with all dependencies', () => { expect(workflowToolManager).toBeDefined(); expect(WorkflowDiscoveryService).toHaveBeenCalledWith( mockApiClient, mockLogger, expect.objectContaining({ enabled: true, discoveryInterval: 0, executionTimeout: 300000, maxConcurrentExecutions: 10, filterPatterns: [], statusCheckInterval: 5000, retryAttempts: 3 }) ); expect(WorkflowExecutionService).toHaveBeenCalledWith( mockApiClient, mockLogger, expect.objectContaining({ executionTimeout: 300000, statusCheckInterval: 5000, maxRetryAttempts: 3 }) ); expect(WorkflowStatusService).toHaveBeenCalledWith( mockApiClient, mockLogger, expect.objectContaining({ statusCheckInterval: 5000, maxRetryAttempts: 3, cleanupInterval: 300000 }) ); }); }); describe('isEnabled', () => { it('should return true when workflows are enabled in config', () => { expect(workflowToolManager.isEnabled()).toBe(true); }); it('should return false when workflows are disabled in config', () => { mockConfig.workflowsEnabled = false; const disabledManager = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); expect(disabledManager.isEnabled()).toBe(false); }); }); describe('initialize', () => { it('should initialize successfully when workflows are enabled and available', async () => { (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await workflowToolManager.initialize(); expect(mockDiscoveryService.isWorkflowsListToolAvailable).toHaveBeenCalled(); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalled(); expect(mockToolRegistry.registerTool).toHaveBeenCalledTimes(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('WorkflowToolManager initialized successfully') ); }); it('should handle disabled workflows gracefully', async () => { mockConfig.workflowsEnabled = false; const disabledManager = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); await disabledManager.initialize(); expect(mockLogger.info).toHaveBeenCalledWith( 'Workflow tools are disabled in configuration' ); expect(mockDiscoveryService.isWorkflowsListToolAvailable).not.toHaveBeenCalled(); }); it('should handle unavailable workflows-list-tool gracefully', async () => { (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(false); await workflowToolManager.initialize(); expect(mockLogger.warn).toHaveBeenCalledWith( 'workflows-list-tool is not available, workflow tools will be disabled' ); expect(mockDiscoveryService.listWorkflows).not.toHaveBeenCalled(); }); it('should handle initialization errors gracefully', async () => { (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockRejectedValue(new Error('Discovery failed')); await workflowToolManager.initialize(); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Failed to initialize WorkflowToolManager') ); }); it('should not initialize twice', async () => { (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([]); await workflowToolManager.initialize(); await workflowToolManager.initialize(); expect(mockLogger.warn).toHaveBeenCalledWith( 'WorkflowToolManager is already initialized' ); }); }); describe('shutdown', () => { it('should shutdown all services and clean up resources', async () => { // Initialize first (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await workflowToolManager.initialize(); // Now shutdown await workflowToolManager.shutdown(); expect(mockStatusService.shutdown).toHaveBeenCalled(); expect(mockExecutionService.shutdown).toHaveBeenCalled(); expect(mockToolRegistry.unregisterTool).toHaveBeenCalledTimes(2); expect(mockLogger.info).toHaveBeenCalledWith( 'WorkflowToolManager shutdown complete' ); }); }); describe('discoverWorkflows', () => { it('should discover workflows successfully', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); const result = await workflowToolManager.discoverWorkflows(); expect(result).toEqual(sampleWorkflows); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalled(); expect(mockLogger.info).toHaveBeenCalledWith( `Discovered ${sampleWorkflows.length} workflows` ); }); it('should return empty array when workflows are disabled', async () => { mockConfig.workflowsEnabled = false; const disabledManager = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); const result = await disabledManager.discoverWorkflows(); expect(result).toEqual([]); expect(mockDiscoveryService.listWorkflows).not.toHaveBeenCalled(); }); it('should handle discovery errors gracefully', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockRejectedValue(new Error('Discovery failed')); await expect(workflowToolManager.discoverWorkflows()).rejects.toThrow('Discovery failed'); }); }); describe('registerWorkflowTools', () => { it('should register workflow tools successfully', () => { workflowToolManager.registerWorkflowTools(sampleWorkflows); expect(mockToolRegistry.registerTool).toHaveBeenCalledTimes(2); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('2 successful, 0 failed') ); }); it('should handle tool registration errors gracefully', () => { (mockToolRegistry.registerTool as jest.MockedFunction<any>) .mockImplementationOnce(() => { throw new Error('Registration failed'); }) .mockImplementationOnce(() => { // Second call succeeds }); workflowToolManager.registerWorkflowTools(sampleWorkflows); expect(mockToolRegistry.registerTool).toHaveBeenCalledTimes(2); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(1); // The error handler logs both structured error and user-friendly message expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Failed to generate tool for workflow test_workflow_1'), expect.any(Object) ); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('1 successful, 1 failed') ); }); it('should replace existing tools with same name', () => { (mockToolRegistry.getTool as jest.MockedFunction<any>) .mockReturnValue({ name: 'workflow_test_workflow_1' }); workflowToolManager.registerWorkflowTools([sampleWorkflows[0]]); expect(mockToolRegistry.unregisterTool).toHaveBeenCalledWith('workflow_test_workflow_1'); expect(mockToolRegistry.registerTool).toHaveBeenCalled(); }); }); describe('refreshWorkflows', () => { it('should refresh workflows successfully', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await workflowToolManager.refreshWorkflows(); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalled(); expect(mockToolRegistry.registerTool).toHaveBeenCalledTimes(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('Workflow refresh completed') ); }); it('should handle refresh errors', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockRejectedValue(new Error('Discovery failed')); await expect(workflowToolManager.refreshWorkflows()).rejects.toThrow('Discovery failed'); }); }); describe('getRegisteredWorkflowCount', () => { it('should return correct count of registered workflows', () => { expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(0); workflowToolManager.registerWorkflowTools(sampleWorkflows); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); }); }); describe('getWorkflowToolNames', () => { it('should return names of registered workflow tools', () => { workflowToolManager.registerWorkflowTools(sampleWorkflows); const toolNames = workflowToolManager.getWorkflowToolNames(); expect(toolNames).toContain('workflow_test_workflow_1_workflow_1'); expect(toolNames).toContain('workflow_test_workflow_2_workflow_2'); expect(toolNames).toHaveLength(2); }); it('should return empty array when no workflows are registered', () => { const toolNames = workflowToolManager.getWorkflowToolNames(); expect(toolNames).toEqual([]); }); }); describe('workflow tool execution', () => { beforeEach(() => { workflowToolManager.registerWorkflowTools([sampleWorkflows[0]]); }); it('should execute workflow tool successfully', async () => { const mockResult: WorkflowExecutionResult = { success: true, correlationId: 'test-correlation-id', workflowInstanceId: 'test-instance-id', originalWorkflowId: '1', status: 'COMPLETED', output: { result: 'success' }, executionDuration: 1000 }; (mockExecutionService.executeWorkflow as jest.MockedFunction<any>) .mockResolvedValue(mockResult); // Get the registered tool and execute it const tool = mockToolRegistry.registerTool.mock.calls[0][0]; const result = await tool.handler({ param1: 'test' }, mockApiClient); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); const resultData = JSON.parse(result.content[0].text); expect(resultData.success).toBe(true); expect(resultData.workflowId).toBe('1'); expect(resultData.workflowName).toBe('test_workflow_1'); expect(resultData.correlationId).toBe('test-correlation-id'); expect(resultData.executionDuration).toBe(1000); expect(resultData.result).toEqual({ result: 'success' }); expect(resultData.status).toBe('COMPLETED'); expect(mockExecutionService.executeWorkflow).toHaveBeenCalledWith('1', { param1: 'test' }); }); it('should handle workflow execution failure', async () => { const mockResult: WorkflowExecutionResult = { success: false, correlationId: 'test-correlation-id', workflowInstanceId: 'test-instance-id', originalWorkflowId: '1', status: 'FAILED', error: 'Execution failed' }; (mockExecutionService.executeWorkflow as jest.MockedFunction<any>) .mockResolvedValue(mockResult); // Get the registered tool and execute it const tool = mockToolRegistry.registerTool.mock.calls[0][0]; const result = await tool.handler({ param1: 'test' }, mockApiClient); expect(result.content).toBeDefined(); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); const resultData = JSON.parse(result.content[0].text); expect(resultData.success).toBe(false); expect(resultData.error).toBe('Execution failed'); expect(resultData.status).toBe('FAILED'); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Workflow tool execution failed') ); }); it('should handle workflow execution errors', async () => { (mockExecutionService.executeWorkflow as jest.MockedFunction<any>) .mockRejectedValue(new Error('Service error')); // Get the registered tool and execute it const tool = mockToolRegistry.registerTool.mock.calls[0][0]; await expect(tool.handler({ param1: 'test' }, mockApiClient)) .rejects.toThrow(AppError); // The error handler now logs with structured format expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Workflow error: EXECUTION_FAILED'), expect.any(Object) ); }); }); describe('tool name generation', () => { it('should generate valid tool names', () => { const workflowWithInvalidName: WorkflowDefinition = { id: '3', name: 'invalid-name!@#$%^&*()', description: 'Test workflow with invalid name', inputSchema: { type: 'object', properties: {} } }; workflowToolManager.registerWorkflowTools([workflowWithInvalidName]); const toolNames = workflowToolManager.getWorkflowToolNames(); expect(toolNames[0]).toMatch(/^[a-zA-Z][a-zA-Z0-9_-]*$/); }); it('should handle name conflicts by appending workflow ID', () => { // First register a workflow to simulate existing tool workflowToolManager.registerWorkflowTools([sampleWorkflows[0]]); // Mock existing tool with same name for the second workflow (mockToolRegistry.getTool as jest.MockedFunction<any>) .mockReturnValue({ name: 'workflow_test_workflow_1' }); // Create a workflow with the same name but different ID const conflictingWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], id: '999', // Different ID name: 'test_workflow_1' // Same name }; workflowToolManager.registerWorkflowTools([conflictingWorkflow]); const toolNames = workflowToolManager.getWorkflowToolNames(); // The second workflow should get a numeric suffix since the first one already exists expect(toolNames).toContain('workflow_test_workflow_1_2'); }); }); describe('auto-refresh functionality', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('should start auto-refresh when interval is configured', async () => { mockConfig.workflowDiscoveryInterval = 60000; // 1 minute const managerWithAutoRefresh = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([]); await managerWithAutoRefresh.initialize(); // Fast-forward time to trigger auto-refresh jest.advanceTimersByTime(60000); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalledTimes(2); // Initial + auto-refresh }); it('should not start auto-refresh when interval is 0', async () => { mockConfig.workflowDiscoveryInterval = 0; (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([]); await workflowToolManager.initialize(); // Fast-forward time jest.advanceTimersByTime(60000); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalledTimes(1); // Only initial call }); }); describe('getWorkflowStats', () => { it('should return correct workflow statistics', () => { workflowToolManager.registerWorkflowTools(sampleWorkflows); const stats = workflowToolManager.getWorkflowStats(); expect(stats).toEqual({ enabled: true, totalWorkflows: 2, lastRefreshTime: expect.any(Number), refreshAge: expect.any(Number), autoRefreshEnabled: false, autoRefreshInterval: 0 }); }); }); describe('getWorkflowById', () => { it('should return workflow by ID', () => { workflowToolManager.registerWorkflowTools(sampleWorkflows); const workflow = workflowToolManager.getWorkflowById('1'); expect(workflow).toEqual(sampleWorkflows[0]); }); it('should return undefined for non-existent workflow', () => { const workflow = workflowToolManager.getWorkflowById('non-existent'); expect(workflow).toBeUndefined(); }); }); describe('getAllWorkflows', () => { it('should return all registered workflows', () => { workflowToolManager.registerWorkflowTools(sampleWorkflows); const workflows = workflowToolManager.getAllWorkflows(); expect(workflows).toEqual(sampleWorkflows); }); }); describe('forceRefresh', () => { it('should clear cache and refresh workflows', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await workflowToolManager.forceRefresh(); expect(mockDiscoveryService.clearCache).toHaveBeenCalled(); expect(mockDiscoveryService.listWorkflows).toHaveBeenCalled(); }); }); describe('getDiscoveryCacheStats', () => { it('should return discovery cache statistics', () => { const mockStats = { cachedCount: 2, lastDiscoveryTime: Date.now(), cacheAge: 1000, isValid: true }; (mockDiscoveryService.getCacheStats as jest.MockedFunction<any>) .mockReturnValue(mockStats); const stats = workflowToolManager.getDiscoveryCacheStats(); expect(stats).toEqual(mockStats); }); }); describe('refresh and hot-reloading functionality', () => { beforeEach(() => { // Initialize with some workflows workflowToolManager.registerWorkflowTools(sampleWorkflows); }); describe('incremental refresh', () => { it('should add new workflows during refresh', async () => { const newWorkflow: WorkflowDefinition = { id: '3', name: 'new_workflow', description: 'New workflow added', inputSchema: { type: 'object', properties: { param3: { type: 'string', description: 'New parameter' } } } }; const updatedWorkflows = [...sampleWorkflows, newWorkflow]; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(updatedWorkflows); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(3); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('1 added, 0 updated, 0 removed, 2 unchanged') ); }); it('should remove workflows that are no longer available', async () => { // Return only the first workflow (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([sampleWorkflows[0]]); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(1); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 0 updated, 1 removed, 1 unchanged') ); }); it('should update workflows that have changed', async () => { const updatedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], description: 'Updated description', version: '2.0.0' }; const updatedWorkflows = [updatedWorkflow, sampleWorkflows[1]]; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(updatedWorkflows); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); it('should detect unchanged workflows correctly', async () => { // Return the same workflows (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 0 updated, 0 removed, 2 unchanged') ); }); it('should handle mixed changes in a single refresh', async () => { const newWorkflow: WorkflowDefinition = { id: '3', name: 'new_workflow', description: 'New workflow', inputSchema: { type: 'object', properties: {} } }; const updatedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], description: 'Updated description' }; // Return new workflow, updated first workflow, skip second workflow const mixedWorkflows = [updatedWorkflow, newWorkflow]; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(mixedWorkflows); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('1 added, 1 updated, 1 removed, 0 unchanged') ); }); it('should handle tool generation errors during refresh', async () => { const problematicWorkflow: WorkflowDefinition = { id: '3', name: 'problematic_workflow', description: 'This will cause an error', inputSchema: { type: 'object', properties: {} } }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([...sampleWorkflows, problematicWorkflow]); // Mock tool generation to fail for the problematic workflow const originalRegisterTool = mockToolRegistry.registerTool; (mockToolRegistry.registerTool as jest.MockedFunction<any>) .mockImplementation((toolDef: any) => { if (toolDef.name.includes('problematic')) { throw new Error('Tool generation failed'); } return originalRegisterTool.call(mockToolRegistry, toolDef); }); await workflowToolManager.refreshWorkflows(); // Should still register the valid workflows expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Failed to generate tool for workflow problematic_workflow'), expect.any(Object) ); }); }); describe('triggerManualRefresh', () => { it('should successfully trigger manual refresh', async () => { const newWorkflow: WorkflowDefinition = { id: '3', name: 'manual_refresh_workflow', description: 'Added via manual refresh', inputSchema: { type: 'object', properties: {} } }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([...sampleWorkflows, newWorkflow]); const result = await workflowToolManager.triggerManualRefresh(); expect(result.success).toBe(true); expect(result.stats).toEqual({ added: 1, updated: 0, removed: 0, unchanged: 2 }); expect(mockLogger.info).toHaveBeenCalledWith('Manual workflow refresh triggered'); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('Manual refresh completed: 1 added, 0 updated, 0 removed, 2 unchanged') ); }); it('should handle manual refresh errors gracefully', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockRejectedValue(new Error('Discovery service unavailable')); const result = await workflowToolManager.triggerManualRefresh(); expect(result.success).toBe(false); expect(result.error).toBe('Discovery service unavailable'); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Manual refresh failed: Discovery service unavailable') ); }); }); describe('getRefreshStatus', () => { it('should return correct refresh status without auto-refresh', () => { const status = workflowToolManager.getRefreshStatus(); expect(status).toEqual({ enabled: true, lastRefreshTime: expect.any(Number), refreshAge: expect.any(Number), autoRefreshEnabled: false, autoRefreshInterval: 0 }); }); it('should return correct refresh status with auto-refresh enabled', async () => { mockConfig.workflowDiscoveryInterval = 60000; const managerWithAutoRefresh = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); // Register workflows and trigger a refresh to set lastRefreshTime managerWithAutoRefresh.registerWorkflowTools(sampleWorkflows); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await managerWithAutoRefresh.refreshWorkflows(); const status = managerWithAutoRefresh.getRefreshStatus(); expect(status).toEqual({ enabled: true, lastRefreshTime: expect.any(Number), refreshAge: expect.any(Number), autoRefreshEnabled: true, autoRefreshInterval: 60000, nextRefreshIn: expect.any(Number) }); expect(status.lastRefreshTime).toBeGreaterThan(0); expect(status.nextRefreshIn).toBeGreaterThanOrEqual(0); }); it('should return disabled status when workflows are disabled', () => { mockConfig.workflowsEnabled = false; const disabledManager = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); const status = disabledManager.getRefreshStatus(); expect(status.enabled).toBe(false); }); }); describe('workflow change detection', () => { it('should detect name changes', async () => { const changedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], name: 'changed_name' }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([changedWorkflow, sampleWorkflows[1]]); await workflowToolManager.refreshWorkflows(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); it('should detect description changes', async () => { const changedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], description: 'Changed description' }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([changedWorkflow, sampleWorkflows[1]]); await workflowToolManager.refreshWorkflows(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); it('should detect input schema changes', async () => { const changedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], inputSchema: { type: 'object', properties: { newParam: { type: 'string', description: 'New parameter' } } } }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([changedWorkflow, sampleWorkflows[1]]); await workflowToolManager.refreshWorkflows(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); it('should detect version changes', async () => { const changedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], version: '2.0.0' }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([changedWorkflow, sampleWorkflows[1]]); await workflowToolManager.refreshWorkflows(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); it('should detect metadata changes', async () => { const changedWorkflow: WorkflowDefinition = { ...sampleWorkflows[0], metadata: { newKey: 'newValue' } }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([changedWorkflow, sampleWorkflows[1]]); await workflowToolManager.refreshWorkflows(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 1 updated, 0 removed, 1 unchanged') ); }); }); describe('auto-refresh with incremental updates', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('should set up auto-refresh timer when interval is configured', async () => { mockConfig.workflowDiscoveryInterval = 60000; const managerWithAutoRefresh = new WorkflowToolManager( mockApiClient, mockLogger, mockConfig, mockToolRegistry ); (mockDiscoveryService.isWorkflowsListToolAvailable as jest.MockedFunction<any>) .mockResolvedValue(true); (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue(sampleWorkflows); await managerWithAutoRefresh.initialize(); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('Starting auto-refresh timer with interval: 60000ms') ); await managerWithAutoRefresh.shutdown(); }); it('should use incremental updates in manual refresh', async () => { // Test incremental updates through manual refresh instead of auto-refresh const newWorkflow: WorkflowDefinition = { id: '3', name: 'manual_refresh_workflow', description: 'Added via manual refresh', inputSchema: { type: 'object', properties: {} } }; (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([...sampleWorkflows, newWorkflow]); const result = await workflowToolManager.triggerManualRefresh(); expect(result.success).toBe(true); expect(result.stats).toEqual({ added: 1, updated: 0, removed: 0, unchanged: 2 }); }); it('should handle refresh errors in manual refresh', async () => { (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockRejectedValue(new Error('Manual refresh discovery failed')); const result = await workflowToolManager.triggerManualRefresh(); expect(result.success).toBe(false); expect(result.error).toBe('Manual refresh discovery failed'); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Manual refresh failed: Manual refresh discovery failed') ); }); }); describe('cleanup and resource management', () => { it('should properly clean up removed workflows', async () => { // Start with 2 workflows expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(2); // Return empty list to simulate all workflows being removed (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([]); await workflowToolManager.refreshWorkflows(); expect(workflowToolManager.getRegisteredWorkflowCount()).toBe(0); expect(mockToolRegistry.unregisterTool).toHaveBeenCalledTimes(2); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('0 added, 0 updated, 2 removed, 0 unchanged') ); }); it('should handle partial cleanup failures gracefully', async () => { // Mock unregisterTool to fail for one tool (mockToolRegistry.unregisterTool as jest.MockedFunction<any>) .mockReturnValueOnce(false) // First call fails .mockReturnValueOnce(true); // Second call succeeds // Return empty list to trigger removal (mockDiscoveryService.listWorkflows as jest.MockedFunction<any>) .mockResolvedValue([]); await workflowToolManager.refreshWorkflows(); // Should still attempt to remove both tools expect(mockToolRegistry.unregisterTool).toHaveBeenCalledTimes(2); }); }); }); });

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