UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

446 lines (388 loc) 13.4 kB
import type { Mock, MockedClass, MockedFunction } from 'vitest'; import { vi } from 'vitest'; import { ActivityExecutor } from '../activity-executor.js'; import { Activity, ProcessExecution, ConditionalBranch, ActivityConfig } from '../types.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; // Mock dependencies vi.mock('@modelcontextprotocol/sdk/server/index.js'); describe('ActivityExecutor', () => { let executor: ActivityExecutor; let mockProcessEngine: any; let mockServer: vi.Mocked<Server>; let mockAgentOrchestrator: any; let mockExecution: ProcessExecution; beforeEach(() => { mockProcessEngine = { emit: vi.fn() }; mockServer = { // Add any server methods that might be called } as any; // Create a simple mock for agent orchestrator mockAgentOrchestrator = { executeSubAgent: vi.fn(), createAgent: vi.fn().mockReturnValue({ id: 'test-agent-123', name: 'analyzer-agent', type: 'analyzer' }), destroyAgent: vi.fn(), createSession: vi.fn().mockReturnValue({ id: 'test-session-123', name: 'Process Activity', status: 'active' }), createTask: vi.fn().mockReturnValue({ id: 'test-task-456', agentTask: 'Analyze code quality', capabilities: {}, dependencies: [] }), executeTask: vi.fn().mockResolvedValue({ success: true, output: 'Analysis completed successfully', artifacts: [], performance: { duration: 1000 } }), completeSession: vi.fn().mockReturnValue({ success: true }) }; executor = new ActivityExecutor(mockProcessEngine, mockAgentOrchestrator); executor.setServer(mockServer); mockExecution = createMockExecution(); }); afterEach(() => { vi.clearAllMocks(); }); describe('execute', () => { describe('tool activities', () => { it('should execute a tool activity successfully', async () => { const activity: Activity = { id: 'tool-1', type: 'tool', name: 'Run Tool', config: { toolName: 'test_tool', toolArgs: { param: 'value' } } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ success: true, toolName: 'test_tool', executedAt: expect.any(String) }); }); it('should throw error if toolName is missing', async () => { const activity: Activity = { id: 'tool-1', type: 'tool', name: 'Invalid Tool', config: {} // Missing toolName }; await expect(executor.execute(activity, {}, mockExecution)) .rejects.toThrow('Tool activity missing toolName'); }); it('should throw error if server not initialized', async () => { const executorNoServer = new ActivityExecutor(mockProcessEngine, mockAgentOrchestrator); const activity = createToolActivity(); await expect(executorNoServer.execute(activity, {}, mockExecution)) .rejects.toThrow('Server not initialized'); }); }); describe('human activities', () => { it('should execute human activity and wait for input', async () => { const activity: Activity = { id: 'human-1', type: 'human', name: 'Approval Required', config: { prompt: 'Please approve this action', assignTo: ['user1', 'user2'], approvalType: 'any' } }; const result = await executor.execute(activity, {}, mockExecution); expect(mockExecution.status).toBe('running'); // Should be set back after wait expect(mockProcessEngine.emit).toHaveBeenCalledWith( 'execution:waiting', expect.objectContaining({ execution: mockExecution, activity }) ); expect(result).toMatchObject({ approved: true, approvedBy: 'user1', approvedAt: expect.any(String) }); }); it('should handle form fields in human activities', async () => { const activity: Activity = { id: 'human-2', type: 'human', name: 'Data Entry', config: { prompt: 'Enter project details', formFields: [ { name: 'projectName', type: 'text', label: 'Project Name', required: true }, { name: 'priority', type: 'select', label: 'Priority', options: ['high', 'medium', 'low'] } ] } }; const result = await executor.execute(activity, {}, mockExecution); expect(result.approved).toBe(true); }); }); describe('agent activities', () => { it('should return fallback result for agent activities (currently not supported)', async () => { const activity: Activity = { id: 'agent-1', type: 'agent', name: 'AI Analysis', config: { agentType: 'analyzer', agentTask: 'Analyze code quality', agentConfig: { threshold: 0.8 } } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ agentType: 'analyzer', agentId: 'fallback-agent', taskId: 'fallback-task', taskCompleted: false, error: 'Agent activities are not currently supported', results: { analysis: 'Agent execution failed - simulated fallback', recommendations: ['Check agent orchestration configuration'] } }); }); }); describe('conditional activities', () => { it('should execute matching branch', async () => { const activity: Activity = { id: 'cond-1', type: 'conditional', name: 'Branch Decision', config: { conditions: [ { condition: 'status === "active"', activities: [createToolActivity('branch1-tool')] }, { condition: 'status === "inactive"', activities: [createToolActivity('branch2-tool')] } ] } }; mockExecution.variables.status = 'active'; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ success: true, toolName: 'test_tool' }); }); it('should execute default branch when no conditions match', async () => { const activity: Activity = { id: 'cond-2', type: 'conditional', name: 'Branch with Default', config: { conditions: [ { condition: 'false', activities: [createToolActivity('never-runs')] } ], defaultBranch: [createToolActivity('default-tool')] } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ success: true, toolName: 'test_tool' }); }); it('should return conditionMatched false when no branch matches', async () => { const activity: Activity = { id: 'cond-3', type: 'conditional', name: 'No Match', config: { conditions: [ { condition: 'false', activities: [] } ] } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toEqual({}); // Returns empty object when no conditions match }); }); describe('loop activities', () => { it('should iterate over collection', async () => { const activity: Activity = { id: 'loop-1', type: 'loop', name: 'Process Items', config: { collection: 'items', itemVariable: 'currentItem', activities: [createToolActivity('process-item')] } }; mockExecution.variables.items = ['item1', 'item2', 'item3']; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ loopCompleted: true, iterations: 3, results: expect.arrayContaining([ expect.objectContaining({ success: true }), expect.objectContaining({ success: true }), expect.objectContaining({ success: true }) ]) }); }); it('should respect maxIterations limit', async () => { const activity: Activity = { id: 'loop-2', type: 'loop', name: 'Limited Loop', config: { collection: 'bigList', maxIterations: 2, activities: [createToolActivity('process')] } }; mockExecution.variables.bigList = [1, 2, 3, 4, 5]; const result = await executor.execute(activity, {}, mockExecution); expect(result.iterations).toBe(2); expect(result.results).toHaveLength(2); }); }); describe('parallel activities', () => { it('should execute all branches in parallel', async () => { const activity: Activity = { id: 'parallel-1', type: 'parallel', name: 'Parallel Tasks', config: { branches: [ [createToolActivity('branch1-act1'), createToolActivity('branch1-act2')], [createToolActivity('branch2-act1')], [createToolActivity('branch3-act1')] ], waitForAll: true } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ parallelCompleted: true, branches: expect.arrayContaining([ expect.objectContaining({ success: true, branchIndex: 0 }), expect.objectContaining({ success: true, branchIndex: 1 }), expect.objectContaining({ success: true, branchIndex: 2 }) ]) }); }); it('should return first completed when waitForAll is false', async () => { const activity: Activity = { id: 'parallel-2', type: 'parallel', name: 'Race Condition', config: { branches: [ [createToolActivity('slow')], [createToolActivity('fast')], ], waitForAll: false } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ parallelCompleted: true, firstCompleted: expect.objectContaining({ success: true }) }); }); }); describe('external activities', () => { it('should make external HTTP request', async () => { const activity: Activity = { id: 'ext-1', type: 'external', name: 'Call API', config: { url: 'https://api.example.com/data', method: 'POST', headers: { 'Content-Type': 'application/json' }, body: { key: 'value' } } }; const result = await executor.execute(activity, {}, mockExecution); expect(result).toMatchObject({ status: 200, response: expect.objectContaining({ message: expect.any(String), data: expect.any(Object) }) }); }); it('should throw error if URL is missing', async () => { const activity: Activity = { id: 'ext-2', type: 'external', name: 'Invalid External', config: {} // Missing URL }; await expect(executor.execute(activity, {}, mockExecution)) .rejects.toThrow('External activity missing URL'); }); }); describe('error handling', () => { it('should throw error for unknown activity type', async () => { const activity = { id: 'unknown-1', type: 'unknown' as any, name: 'Unknown Type', config: {} }; await expect(executor.execute(activity, {}, mockExecution)) .rejects.toThrow('Unknown activity type: unknown'); }); }); }); }); // Helper functions function createMockExecution(): ProcessExecution { return { id: 'exec-123', processId: 'process-123', processVersion: '1.0.0', status: 'running', triggeredBy: 'manual', startedAt: new Date().toISOString(), variables: {}, activityResults: [], logs: [] }; } function createToolActivity(id: string = 'tool-1'): Activity { return { id, type: 'tool', name: 'Test Tool', config: { toolName: 'test_tool', toolArgs: {} } }; }