Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
workflow-executor.test.ts14.4 kB
/** * @fileoverview Unit tests for workflow execution engine * * Tests the main workflow orchestration logic including task execution, * state management, SSE events, deliverable collection, and error handling * with fully mocked dependencies. * * @since 1.0.0 */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { executeWorkflowFromJSON, executionStates } from './workflow-executor.js'; import type { IGraphManager } from '../../types/index.js'; import * as persistence from './persistence.js'; import * as sse from './sse.js'; import * as taskExecutor from '../../orchestrator/task-executor.js'; // Mock modules vi.mock('./persistence.js'); vi.mock('./sse.js'); vi.mock('../../orchestrator/task-executor.js'); vi.mock('fs/promises', () => ({ writeFile: vi.fn().mockResolvedValue(undefined), readdir: vi.fn().mockResolvedValue([]), stat: vi.fn().mockResolvedValue({ isFile: () => true }), readFile: vi.fn().mockResolvedValue('file content'), rm: vi.fn().mockResolvedValue(undefined), })); describe('Workflow Executor', () => { let mockGraphManager: IGraphManager; let mockSession: any; let mockDriver: any; beforeEach(() => { // Clear all mocks vi.clearAllMocks(); // Clear execution states executionStates.clear(); // Create mock session mockSession = { run: vi.fn().mockResolvedValue({ records: [] }), close: vi.fn().mockResolvedValue(undefined), }; // Create mock driver mockDriver = { session: vi.fn().mockReturnValue(mockSession), }; // Create mock graph manager mockGraphManager = { getDriver: vi.fn().mockReturnValue(mockDriver), } as any; // Mock persistence functions vi.mocked(persistence.createExecutionNodeInNeo4j).mockResolvedValue(undefined); vi.mocked(persistence.persistTaskExecutionToNeo4j).mockResolvedValue('task-exec-1'); vi.mocked(persistence.updateExecutionNodeProgress).mockResolvedValue(undefined); vi.mocked(persistence.updateExecutionNodeInNeo4j).mockResolvedValue(undefined); // Mock SSE functions vi.mocked(sse.sendSSEEvent).mockImplementation(() => {}); vi.mocked(sse.closeSSEConnections).mockImplementation(() => {}); // Mock task execution functions vi.mocked(taskExecutor.generatePreamble).mockResolvedValue('# Test Preamble\nContent...'); vi.mocked(taskExecutor.executeTask).mockResolvedValue({ taskId: 'task-1', status: 'success', output: 'Task completed', duration: 1000, preamblePath: '/test.md', tokens: { input: 100, output: 50 }, toolCalls: 2, }); }); afterEach(() => { vi.clearAllTimers(); }); describe('executeWorkflowFromJSON', () => { it('should execute a simple single-task workflow successfully', async () => { const uiTasks = [ { id: 'task-1', title: 'Test Task', prompt: 'Do something', agentRoleDescription: 'Test Worker', dependencies: [], }, ]; const executionId = 'exec-123'; const results = await executeWorkflowFromJSON( uiTasks, executionId, mockGraphManager ); expect(results).toHaveLength(1); expect(results[0].status).toBe('success'); expect(results[0].taskId).toBe('task-1'); // Verify execution node was created expect(persistence.createExecutionNodeInNeo4j).toHaveBeenCalledWith( mockGraphManager, executionId, executionId, 1, expect.any(Number) ); // Verify task was persisted expect(persistence.persistTaskExecutionToNeo4j).toHaveBeenCalledOnce(); // Verify execution was finalized expect(persistence.updateExecutionNodeInNeo4j).toHaveBeenCalledWith( mockGraphManager, executionId, results, expect.any(Number), false ); }); it('should initialize execution state correctly', async () => { const uiTasks = [ { id: 'task-1', title: 'Task 1', prompt: 'Do A', agentRoleDescription: 'Worker A', dependencies: [], }, { id: 'task-2', title: 'Task 2', prompt: 'Do B', agentRoleDescription: 'Worker B', dependencies: ['task-1'], }, ]; const executionId = 'exec-456'; await executeWorkflowFromJSON(uiTasks, executionId, mockGraphManager); const state = executionStates.get(executionId); expect(state).toBeDefined(); expect(state?.status).toBe('completed'); expect(state?.taskStatuses['task-1']).toBe('completed'); expect(state?.taskStatuses['task-2']).toBe('completed'); }); it('should send SSE events for execution lifecycle', async () => { const uiTasks = [ { id: 'task-1', title: 'Test Task', prompt: 'Test', agentRoleDescription: 'Worker', dependencies: [], }, ]; const executionId = 'exec-789'; await executeWorkflowFromJSON(uiTasks, executionId, mockGraphManager); // Verify SSE events were sent expect(sse.sendSSEEvent).toHaveBeenCalledWith( executionId, 'execution-start', expect.any(Object) ); expect(sse.sendSSEEvent).toHaveBeenCalledWith( executionId, 'task-start', expect.any(Object) ); expect(sse.sendSSEEvent).toHaveBeenCalledWith( executionId, 'task-complete', expect.any(Object) ); expect(sse.sendSSEEvent).toHaveBeenCalledWith( executionId, 'execution-complete', expect.any(Object) ); }); it('should handle task execution failure gracefully', async () => { // Mock task failure vi.mocked(taskExecutor.executeTask).mockResolvedValueOnce({ taskId: 'task-1', status: 'failure', output: '', error: 'Task failed', duration: 500, preamblePath: '/test.md', }); const uiTasks = [ { id: 'task-1', title: 'Failing Task', prompt: 'Fail', agentRoleDescription: 'Worker', dependencies: [], }, ]; const executionId = 'exec-fail'; const results = await executeWorkflowFromJSON( uiTasks, executionId, mockGraphManager ); expect(results).toHaveLength(1); expect(results[0].status).toBe('failure'); expect(results[0].error).toBe('Task failed'); // Verify execution was marked as failed const state = executionStates.get(executionId); expect(state?.status).toBe('failed'); }); it('should stop execution after first task failure', async () => { // First task fails vi.mocked(taskExecutor.executeTask) .mockResolvedValueOnce({ taskId: 'task-1', status: 'failure', output: '', error: 'Failed', duration: 500, preamblePath: '/test.md', }) .mockResolvedValueOnce({ taskId: 'task-2', status: 'success', output: 'Should not execute', duration: 1000, preamblePath: '/test.md', }); const uiTasks = [ { id: 'task-1', title: 'Task 1', prompt: 'A', agentRoleDescription: 'Worker', dependencies: [] }, { id: 'task-2', title: 'Task 2', prompt: 'B', agentRoleDescription: 'Worker', dependencies: ['task-1'] }, ]; const results = await executeWorkflowFromJSON( uiTasks, 'exec-stop', mockGraphManager ); // Only first task should have executed expect(results).toHaveLength(1); expect(results[0].taskId).toBe('task-1'); expect(taskExecutor.executeTask).toHaveBeenCalledOnce(); }); it('should generate preambles for each unique role', async () => { const uiTasks = [ { id: 'task-1', title: 'Task 1', prompt: 'A', agentRoleDescription: 'Worker A', dependencies: [] }, { id: 'task-2', title: 'Task 2', prompt: 'B', agentRoleDescription: 'Worker A', dependencies: [] }, { id: 'task-3', title: 'Task 3', prompt: 'C', agentRoleDescription: 'Worker B', dependencies: [] }, ]; await executeWorkflowFromJSON(uiTasks, 'exec-roles', mockGraphManager); // Should generate 2 preambles (one for each unique role) expect(taskExecutor.generatePreamble).toHaveBeenCalledTimes(2); }); it('should include QC preambles when QC roles are specified', async () => { const uiTasks = [ { id: 'task-1', title: 'Task with QC', prompt: 'Test', agentRoleDescription: 'Worker', qcRole: 'QC Agent', verificationCriteria: 'Check A, Check B', dependencies: [], }, ]; await executeWorkflowFromJSON(uiTasks, 'exec-qc', mockGraphManager); // Should generate both worker and QC preambles expect(taskExecutor.generatePreamble).toHaveBeenCalledWith( 'Worker', expect.any(String), expect.any(Object), false ); expect(taskExecutor.generatePreamble).toHaveBeenCalledWith( 'QC Agent', expect.any(String), expect.any(Object), true ); }); it('should handle execution cancellation', async () => { const executionId = 'exec-cancel'; const uiTasks = [ { id: 'task-1', title: 'Task 1', prompt: 'A', agentRoleDescription: 'Worker', dependencies: [] }, ]; // Start execution const executionPromise = executeWorkflowFromJSON( uiTasks, executionId, mockGraphManager ); // Cancel execution mid-flight const state = executionStates.get(executionId); if (state) { state.cancelled = true; } await executionPromise; // Verify cancelled status expect(sse.sendSSEEvent).toHaveBeenCalledWith( executionId, 'execution-cancelled', expect.any(Object) ); }); it('should update progress after each task completion', async () => { const uiTasks = [ { id: 'task-1', title: 'Task 1', prompt: 'A', agentRoleDescription: 'Worker', dependencies: [] }, { id: 'task-2', title: 'Task 2', prompt: 'B', agentRoleDescription: 'Worker', dependencies: [] }, ]; await executeWorkflowFromJSON(uiTasks, 'exec-progress', mockGraphManager); // Should update progress twice (once per task) expect(persistence.updateExecutionNodeProgress).toHaveBeenCalledTimes(2); }); it('should close SSE connections after completion', async () => { vi.useFakeTimers(); const uiTasks = [ { id: 'task-1', title: 'Task', prompt: 'Test', agentRoleDescription: 'Worker', dependencies: [] }, ]; const executionId = 'exec-sse'; await executeWorkflowFromJSON(uiTasks, executionId, mockGraphManager); // Fast-forward timers to trigger closeSSEConnections vi.advanceTimersByTime(1000); expect(sse.closeSSEConnections).toHaveBeenCalledWith(executionId); vi.useRealTimers(); }); it('should handle Neo4j persistence failures gracefully', async () => { // Mock persistence failure vi.mocked(persistence.persistTaskExecutionToNeo4j).mockRejectedValueOnce( new Error('Neo4j connection failed') ); const uiTasks = [ { id: 'task-1', title: 'Task', prompt: 'Test', agentRoleDescription: 'Worker', dependencies: [] }, ]; // Should not throw - execution continues even if persistence fails const results = await executeWorkflowFromJSON( uiTasks, 'exec-persist-fail', mockGraphManager ); expect(results).toHaveLength(1); expect(results[0].status).toBe('success'); }); it('should convert UI tasks to TaskDefinition format correctly', async () => { const uiTasks = [ { id: 'task-1', title: 'Complex Task', prompt: 'Do complex thing', agentRoleDescription: 'Complex Worker', recommendedModel: 'gpt-4-turbo', estimatedDuration: '15 min', estimatedToolCalls: 10, dependencies: ['task-0'], parallelGroup: 'group-1', qcRole: 'Complex QC', verificationCriteria: 'Criterion 1, Criterion 2', maxRetries: 3, }, ]; await executeWorkflowFromJSON(uiTasks, 'exec-complex', mockGraphManager); // Verify executeTask was called with properly formatted TaskDefinition expect(taskExecutor.executeTask).toHaveBeenCalledWith( expect.objectContaining({ id: 'task-1', title: 'Complex Task', agentRoleDescription: 'Complex Worker', recommendedModel: 'gpt-4-turbo', prompt: 'Do complex thing', dependencies: ['task-0'], estimatedDuration: '15 min', parallelGroup: 'group-1', qcRole: 'Complex QC', verificationCriteria: 'Criterion 1, Criterion 2', // Keep as comma-separated maxRetries: 3, estimatedToolCalls: 10, taskType: 'agent', // New field for task type lambdaId: undefined, lambdaScript: undefined, lambdaLanguage: undefined, lambdaName: undefined, }), expect.any(String), expect.any(String), expect.any(String), expect.any(Function), expect.objectContaining({ executionId: 'exec-complex', isCancelled: false, }) ); }); }); describe('executionStates', () => { it('should maintain execution state throughout workflow', async () => { const executionId = 'exec-state-test'; const uiTasks = [ { id: 'task-1', title: 'Task', prompt: 'Test', agentRoleDescription: 'Worker', dependencies: [] }, ]; await executeWorkflowFromJSON(uiTasks, executionId, mockGraphManager); const state = executionStates.get(executionId); expect(state).toBeDefined(); expect(state?.executionId).toBe(executionId); expect(state?.results).toHaveLength(1); expect(state?.startTime).toBeDefined(); expect(state?.endTime).toBeDefined(); }); }); });

Latest Blog Posts

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/orneryd/Mimir'

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