UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

283 lines (238 loc) 9.22 kB
import type { Mock, MockedClass, MockedFunction } from 'vitest'; import { vi } from 'vitest'; import { TriggerSuggestionEngine } from '../trigger-suggestion-engine.js'; import { ProcessStore } from '../process-store.js'; import { ProcessDefinition, Activity, PersonaType, TriggerSuggestion } from '../types.js'; // Mock the store vi.mock('../process-store.js'); describe('TriggerSuggestionEngine', () => { let engine: TriggerSuggestionEngine; let mockStore: vi.Mocked<ProcessStore>; beforeEach(() => { mockStore = { getAllProcesses: vi.fn().mockResolvedValue([]) } as any; engine = new TriggerSuggestionEngine(mockStore); }); afterEach(() => { vi.clearAllMocks(); }); describe('suggestTriggers', () => { it('should suggest schedule trigger for report generation', async () => { const process = createProcess({ name: 'Weekly Sales Report', activities: [ createActivity('generate-report', 'Generate Report'), createActivity('send-email', 'Send Email') ] }); const suggestions = await engine.suggestTriggers(process); expect(suggestions.length).toBeGreaterThan(0); expect(suggestions[0].trigger.type).toBe('schedule'); expect(suggestions[0].confidence).toBeGreaterThan(0.8); expect(suggestions[0].reasoning).toContain('Report generation'); }); it('should suggest continuous monitoring schedule', async () => { const process = createProcess({ name: 'System Health Monitor', activities: [ createActivity('check-status', 'Monitor System Status'), createActivity('alert', 'Send Alert if Down') ] }); const suggestions = await engine.suggestTriggers(process); expect(suggestions[0].trigger.type).toBe('schedule'); expect(suggestions[0].trigger.config.cron).toBe('*/15 * * * *'); // Every 15 minutes expect(suggestions[0].reasoning).toContain('Continuous monitoring'); }); it('should suggest review schedule with human interaction', async () => { const process = createProcess({ name: 'Code Review Process', activities: [ createActivity('gather-prs', 'Gather Pull Requests'), createHumanActivity('review', 'Review Code'), createActivity('update-status', 'Update PR Status') ] }); const suggestions = await engine.suggestTriggers(process); expect(suggestions[0].trigger.type).toBe('schedule'); expect(suggestions[0].trigger.config.cron).toBe('0 9 * * 1-5'); // Weekdays at 9 AM expect(suggestions[0].reasoning).toContain('business hours'); }); it('should use persona-specific patterns', async () => { const process = createProcess({ persona: 'software-engineer', name: 'Development Tasks', activities: [ createActivity('check-ci', 'Check CI Status'), createActivity('run-tests', 'Run Tests') ] }); const suggestions = await engine.suggestTriggers(process); // Should include both schedule and event triggers for engineers const scheduleSuggestion = suggestions.find(s => s.trigger.type === 'schedule'); const eventSuggestion = suggestions.find(s => s.trigger.type === 'event'); expect(scheduleSuggestion).toBeDefined(); expect(eventSuggestion).toBeDefined(); expect(scheduleSuggestion?.reasoning).toContain('Daily development tasks'); }); it('should suggest event trigger for data processing', async () => { const process = createProcess({ name: 'Data ETL Pipeline', activities: [ createActivity('extract', 'Extract Data'), createActivity('transform', 'Transform Data'), createActivity('load', 'Load to Database') ] }); const suggestions = await engine.suggestTriggers(process); // Should include event-based trigger as alternative const eventSuggestion = suggestions.find(s => s.trigger.type === 'event'); expect(eventSuggestion).toBeDefined(); expect(eventSuggestion?.reasoning).toContain('data changes'); }); it('should detect scheduling conflicts', async () => { // Mock existing process with same schedule const existingProcess = createProcess({ id: 'existing-123', name: 'Existing Daily Task', triggers: [{ id: 'trigger-1', type: 'schedule', name: 'Daily', enabled: true, config: { cron: '0 9 * * *' } }] }); mockStore.getAllProcesses.mockResolvedValue([existingProcess]); const newProcess = createProcess({ name: 'New Daily Task', activities: [createActivity('task', 'Daily Task')] }); const suggestions = await engine.suggestTriggers(newProcess); // Check if conflicts are detected const scheduleSuggestion = suggestions.find(s => s.trigger.type === 'schedule' && s.trigger.config.cron === '0 9 * * *' ); if (scheduleSuggestion && scheduleSuggestion.conflicts) { expect(scheduleSuggestion.conflicts.length).toBeGreaterThan(0); expect(scheduleSuggestion.conflicts[0].conflictType).toBe('time'); expect(scheduleSuggestion.conflicts[0].suggestion).toContain('offset'); } }); it('should determine appropriate schedule based on process name', async () => { const testCases = [ { name: 'Daily Standup Notes', expectedCron: '0 8 * * *', description: 'daily' }, { name: 'Weekly Team Report', expectedCron: '0 8 * * 1', description: 'weekly' }, { name: 'Monthly Financial Review', expectedCron: '0 8 1 * *', description: 'monthly' } ]; for (const testCase of testCases) { const process = createProcess({ name: testCase.name, activities: [createActivity('generate', 'Generate Report')] }); const suggestions = await engine.suggestTriggers(process); expect(suggestions[0].trigger.config.cron).toBe(testCase.expectedCron); expect(suggestions[0].reasoning).toContain(testCase.description); } }); it('should suggest manual trigger as fallback', async () => { const process = createProcess({ name: 'Miscellaneous Task', activities: [createActivity('task', 'Do Something')] }); const suggestions = await engine.suggestTriggers(process); // Should have manual trigger when no clear pattern const manualSuggestion = suggestions.find(s => s.trigger.type === 'manual'); expect(manualSuggestion).toBeDefined(); expect(manualSuggestion?.confidence).toBeLessThan(0.6); expect(manualSuggestion?.reasoning).toContain('flexibility'); }); it('should generate alternatives', async () => { const process = createProcess({ name: 'Data Processing Job', activities: [ createActivity('process', 'Process Data'), createExternalActivity('api', 'Call External API') ] }); const suggestions = await engine.suggestTriggers(process); // Should have multiple suggestions expect(suggestions.length).toBeGreaterThan(1); // Should include different trigger types const triggerTypes = suggestions.map(s => s.trigger.type); expect(triggerTypes).toContain('schedule'); expect(triggerTypes).toContain('event'); }); it('should sort suggestions by confidence', async () => { const process = createProcess({ name: 'Complex Workflow', activities: [ createActivity('step1', 'Step 1'), createActivity('step2', 'Step 2') ] }); const suggestions = await engine.suggestTriggers(process); // Verify sorted by confidence (descending) for (let i = 1; i < suggestions.length; i++) { expect(suggestions[i-1].confidence).toBeGreaterThanOrEqual(suggestions[i].confidence); } }); }); }); // Helper functions function createProcess(overrides: Partial<ProcessDefinition> = {}): ProcessDefinition { return { id: 'process-test-123', name: 'Test Process', version: '1.0.0', triggers: [], activities: [], variables: {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0 }, ...overrides }; } function createActivity(id: string, name: string): Activity { return { id, type: 'tool', name, config: { toolName: 'test_tool', toolArgs: {} } }; } function createHumanActivity(id: string, name: string): Activity { return { id, type: 'human', name, config: { prompt: 'Please review', approvalType: 'any' } }; } function createExternalActivity(id: string, name: string): Activity { return { id, type: 'external', name, config: { url: 'https://api.example.com', method: 'GET' } }; }