UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

371 lines (301 loc) 12.6 kB
import type { Mock, MockedClass, MockedFunction } from 'vitest'; import { vi } from 'vitest'; import { EventEmitter } from 'events'; import { ProcessEngine } from '../process-engine.js'; import { ProcessStore } from '../process-store.js'; import { AgentOrchestrator } from '../../agent-orchestration/orchestrator.js'; import { ProcessDefinition, ProcessExecution, Activity, ExecutionStatus } from '../types.js'; // Mock the dependencies vi.mock('../process-store.js'); // Mock ActivityExecutor const mockActivityExecutor = { execute: vi.fn().mockResolvedValue({ success: true, output: 'Activity completed successfully', executedAt: new Date().toISOString() }), setServer: vi.fn() }; vi.mock('../activity-executor.js', () => ({ ActivityExecutor: vi.fn(() => mockActivityExecutor) })); // Mock VariableResolver const mockVariableResolver = { resolveVariables: vi.fn().mockImplementation((vars) => Promise.resolve(vars)), evaluateCondition: vi.fn().mockResolvedValue(true), interpolateString: vi.fn().mockImplementation((str) => str) }; vi.mock('../variable-resolver.js', () => ({ VariableResolver: vi.fn(() => mockVariableResolver) })); vi.mock('../../agent-orchestration/orchestrator.js'); describe('ProcessEngine', () => { describe('Constructor and basic methods', () => { it('should create ProcessEngine instance', () => { const store = {} as any; const orchestrator = {} as any; const engine = new ProcessEngine(store, orchestrator); expect(engine).toBeInstanceOf(ProcessEngine); expect(engine).toBeInstanceOf(EventEmitter); }); }); let engine: ProcessEngine; let mockStore: vi.Mocked<ProcessStore>; let mockAgentOrchestrator: vi.Mocked<AgentOrchestrator>; let mockExecution: ProcessExecution; beforeEach(() => { mockStore = { saveExecution: vi.fn().mockResolvedValue(undefined), getProcess: vi.fn(), saveProcess: vi.fn() } as any; mockAgentOrchestrator = { createSession: vi.fn(), createAgent: vi.fn(), createTask: vi.fn(), executeTask: vi.fn() } as any; // Reset and configure mocks mockActivityExecutor.execute.mockReset(); mockActivityExecutor.execute.mockResolvedValue({ success: true, output: 'Activity completed successfully', executedAt: new Date().toISOString() }); mockVariableResolver.resolveVariables.mockReset(); mockVariableResolver.resolveVariables.mockImplementation((vars) => Promise.resolve(vars)); mockVariableResolver.evaluateCondition.mockReset(); mockVariableResolver.evaluateCondition.mockResolvedValue(true); engine = new ProcessEngine(mockStore, mockAgentOrchestrator); mockExecution = { id: 'exec-123', processId: 'process-123', processVersion: '1.0.0', status: 'pending', triggeredBy: 'manual', startedAt: new Date().toISOString(), variables: {}, activityResults: [], logs: [] }; }); afterEach(() => { vi.clearAllMocks(); }); describe('executeProcess', () => { it('should create and execute a process successfully', async () => { const process: ProcessDefinition = createTestProcess(); const execution = await engine.executeProcess(process); // Debug output if (execution.status === 'failed') { console.error('Execution failed with error:', execution.error); console.error('Activity results:', execution.activityResults); } expect(execution.status).toBe('completed'); expect(execution.processId).toBe(process.id); expect(execution.activityResults).toHaveLength(2); expect(mockStore.saveExecution).toHaveBeenCalled(); }); it('should handle empty activities', async () => { const process: ProcessDefinition = createTestProcess({ activities: [] }); const execution = await engine.executeProcess(process); expect(execution.status).toBe('completed'); expect(execution.activityResults).toHaveLength(0); }); it('should pass initial variables to execution', async () => { const process = createTestProcess(); const initialVars = { foo: 'bar', count: 42 }; const execution = await engine.executeProcess(process, 'manual', initialVars); expect(execution.variables).toMatchObject(initialVars); }); it('should execute activities in sequence', async () => { const activities: Activity[] = [ createTestActivity('act-1', 'First'), createTestActivity('act-2', 'Second'), createTestActivity('act-3', 'Third') ]; const process = createTestProcess({ activities }); const execution = await engine.executeProcess(process); // Verify activities executed in order expect(execution.activityResults[0].activityId).toBe('act-1'); expect(execution.activityResults[1].activityId).toBe('act-2'); expect(execution.activityResults[2].activityId).toBe('act-3'); }); it('should skip activities when condition is false', async () => { const activities: Activity[] = [ createTestActivity('act-1', 'Always runs'), createTestActivity('act-2', 'Should skip', { condition: 'false' }), createTestActivity('act-3', 'Also runs') ]; // Configure mock to return false for the second activity's condition mockVariableResolver.evaluateCondition.mockImplementation((condition) => { return Promise.resolve(condition !== 'false'); }); const process = createTestProcess({ activities }); const execution = await engine.executeProcess(process); expect(execution.activityResults).toHaveLength(3); expect(execution.activityResults[1].status).toBe('skipped'); }); it('should handle activity failures', async () => { const failingActivity = createTestActivity('fail-1', 'Will fail'); // Mock the activity executor to throw an error for this activity mockActivityExecutor.execute.mockRejectedValueOnce(new Error('Activity failed')); const process = createTestProcess({ activities: [failingActivity] }); const execution = await engine.executeProcess(process); expect(execution.status).toBe('failed'); expect(execution.error).toBeDefined(); expect(execution.error?.message).toBe('Activity failed'); }); it('should execute success handlers on completion', async () => { const successActivity = createTestActivity('success-1', 'Success handler'); const process = createTestProcess({ activities: [createTestActivity('main-1', 'Main')], onSuccess: [successActivity] }); const execution = await engine.executeProcess(process); expect(execution.status).toBe('completed'); // Should have main activity + success handler expect(execution.activityResults).toHaveLength(2); expect(execution.activityResults[1].activityId).toBe('success-1'); }); it('should execute failure handlers on error', async () => { const failureActivity = createTestActivity('failure-1', 'Failure handler'); const process = createTestProcess({ activities: [createTestActivity('fail-1', 'Will fail')], onFailure: [failureActivity] }); // Mock activity executor to fail for the main activity, succeed for handler mockActivityExecutor.execute .mockRejectedValueOnce(new Error('Activity failed')) .mockResolvedValueOnce({ success: true, output: 'Failure handled' }); const execution = await engine.executeProcess(process); // Process should be failed expect(execution.status).toBe('failed'); expect(execution.error).toBeDefined(); // Should have executed both the main activity (failed) and failure handler expect(mockActivityExecutor.execute).toHaveBeenCalledTimes(2); // Verify failure handler was called const secondCall = mockActivityExecutor.execute.mock.calls[1]; expect(secondCall[0].id).toBe('failure-1'); }); it('should emit lifecycle events', async () => { const process = createTestProcess(); const events: string[] = []; engine.on('execution:started', () => events.push('started')); engine.on('activity:started', () => events.push('activity:started')); engine.on('activity:completed', () => events.push('activity:completed')); engine.on('execution:completed', () => events.push('completed')); await engine.executeProcess(process); expect(events).toContain('started'); expect(events).toContain('completed'); expect(events.filter(e => e === 'activity:started')).toHaveLength(2); expect(events.filter(e => e === 'activity:completed')).toHaveLength(2); }); }); describe('pauseExecution', () => { it('should pause a running execution', async () => { const process = createTestProcess(); // Create a slow activity to ensure we can pause during execution const slowActivity = createTestActivity('slow-1', 'Slow Activity'); process.activities = [slowActivity]; // Mock the activity executor to take some time let resolveFn: any; const slowPromise = new Promise((resolve) => { resolveFn = resolve; }); mockActivityExecutor.execute.mockReturnValueOnce(slowPromise); // Start execution in background const executionPromise = engine.executeProcess(process); // Wait a bit to ensure execution starts await new Promise(resolve => setTimeout(resolve, 10)); // Get the execution ID from the engine's internal map const executions = Array.from((engine as any).executions.values()); expect(executions.length).toBe(1); const executionId = executions[0].id; // Pause the execution await engine.pauseExecution(executionId); const execution = engine.getExecution(executionId); expect(execution?.status).toBe('paused'); // Clean up by resolving the slow activity resolveFn({ success: true }); await executionPromise; }); }); describe('cancelExecution', () => { it('should cancel a running execution', async () => { const process = createTestProcess(); // Create a slow activity const slowActivity = createTestActivity('slow-1', 'Slow Activity'); process.activities = [slowActivity]; // Mock the activity executor to take some time let rejectFn: any; const slowPromise = new Promise((resolve, reject) => { rejectFn = reject; }); mockActivityExecutor.execute.mockReturnValueOnce(slowPromise); // Start execution in background const executionPromise = engine.executeProcess(process).catch(() => { // Expect this to fail due to cancellation }); // Wait a bit to ensure execution starts await new Promise(resolve => setTimeout(resolve, 10)); // Get the execution ID const executions = Array.from((engine as any).executions.values()); const executionId = executions[0].id; // Cancel the execution await engine.cancelExecution(executionId); const execution = engine.getExecution(executionId); expect(execution?.status).toBe('cancelled'); expect(mockStore.saveExecution).toHaveBeenCalled(); // Clean up rejectFn(new Error('Cancelled')); await executionPromise; }); }); }); // Test helper functions function createTestProcess(overrides: Partial<ProcessDefinition> = {}): ProcessDefinition { return { id: 'process-123', name: 'Test Process', version: '1.0.0', triggers: [], activities: [ createTestActivity('act-1', 'Activity 1'), createTestActivity('act-2', 'Activity 2') ], variables: {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0 }, ...overrides }; } function createTestActivity( id: string, name: string, overrides: Partial<Activity> = {} ): Activity { return { id, type: 'tool', name, config: { toolName: 'test_tool', toolArgs: {} }, ...overrides }; }