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