@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
371 lines (301 loc) • 12.6 kB
text/typescript
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
};
}