UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

384 lines (334 loc) 12.8 kB
import { describe, it, expect, beforeEach } from 'vitest'; import { PrioritizationEngine } from '../prioritization-engine.js'; import { Feature, PrioritizationCriteria, PrioritizationResult, Initiative, ValueMetrics, EffortEstimate } from '../types.js'; describe('PrioritizationEngine', () => { let engine: PrioritizationEngine; let testFeatures: Feature[]; beforeEach(() => { engine = new PrioritizationEngine(); // Create test features with different characteristics testFeatures = [ { id: 'feature-1', initiativeId: 'init-1', name: 'High Value Low Complexity Feature', description: 'A feature with high business value and low complexity', userStories: [], priority: 0, businessValue: { score: 90, rationale: 'Critical for user retention', metrics: ['User retention > 80%', 'Daily active users +50%'] }, technicalComplexity: 'low', status: 'proposed' }, { id: 'feature-2', initiativeId: 'init-1', name: 'Medium Value Medium Complexity Feature', description: 'A balanced feature with medium value and complexity', userStories: [], priority: 0, businessValue: { score: 60, rationale: 'Nice to have enhancement', metrics: ['User satisfaction +10%'] }, technicalComplexity: 'medium', status: 'proposed' }, { id: 'feature-3', initiativeId: 'init-1', name: 'High Value High Complexity Feature', description: 'A valuable but complex feature requiring significant effort', userStories: [], priority: 0, businessValue: { score: 85, rationale: 'Strategic differentiator', metrics: ['Market share +5%', 'Revenue +$1M'] }, technicalComplexity: 'very-high', status: 'approved' }, { id: 'feature-4', initiativeId: 'init-1', name: 'Low Value Feature', description: 'A feature with limited business impact', userStories: [], priority: 0, businessValue: { score: 25, rationale: 'Minor improvement', metrics: ['Support tickets -5%'] }, technicalComplexity: 'low', status: 'proposed' } ]; }); describe('RICE Scoring', () => { it('should prioritize features using RICE method', async () => { const criteria: PrioritizationCriteria = { method: 'rice' }; const results = await engine.prioritizeFeatures(testFeatures, criteria); expect(results).toHaveLength(testFeatures.length); expect(results[0].method).toBe('rice'); // Features should be ranked 1 to N results.forEach((result, index) => { expect(result.rank).toBe(index + 1); }); // High value, low complexity should rank higher const highValueLowComplexity = results.find(r => r.featureId === 'feature-1'); const lowValue = results.find(r => r.featureId === 'feature-4'); expect(highValueLowComplexity!.rank).toBeLessThan(lowValue!.rank); expect(highValueLowComplexity!.score).toBeGreaterThan(lowValue!.score); }); it('should include RICE breakdown in results', async () => { const criteria: PrioritizationCriteria = { method: 'rice' }; const results = await engine.prioritizeFeatures(testFeatures, criteria); const result = results[0]; expect(result.breakdown).toBeDefined(); expect(result.breakdown.reach).toBeGreaterThan(0); expect(result.breakdown.impact).toBeGreaterThan(0); expect(result.breakdown.confidence).toBeGreaterThan(0); expect(result.breakdown.confidence).toBeLessThanOrEqual(1); expect(result.breakdown.effort).toBeGreaterThan(0); expect(result.breakdown.score).toBe( (result.breakdown.reach * result.breakdown.impact * result.breakdown.confidence) / result.breakdown.effort ); }); }); describe('Value-Effort Matrix', () => { it('should prioritize features using value-effort matrix', async () => { const criteria: PrioritizationCriteria = { method: 'value-effort' }; const results = await engine.prioritizeFeatures(testFeatures, criteria); expect(results).toHaveLength(testFeatures.length); // Quick wins (high value, low effort) should rank highest const quickWin = results.find(r => r.featureId === 'feature-1'); expect(quickWin!.rank).toBe(1); expect(quickWin!.breakdown.category).toBe('Quick Wins'); }); it('should categorize features correctly', async () => { const criteria: PrioritizationCriteria = { method: 'value-effort' }; const results = await engine.prioritizeFeatures(testFeatures, criteria); results.forEach(result => { const feature = testFeatures.find(f => f.id === result.featureId); expect(result.breakdown.category).toMatch(/Quick Wins|Major Projects|Fill-ins|Time Sinks/); // Verify categorization logic if (feature!.businessValue.score > 60 && ['low', 'medium'].includes(feature!.technicalComplexity)) { expect(['Quick Wins', 'Major Projects']).toContain(result.breakdown.category); } }); }); }); describe('MoSCoW Method', () => { it('should prioritize features using MoSCoW method', async () => { const criteria: PrioritizationCriteria = { method: 'moscow' }; const results = await engine.prioritizeFeatures(testFeatures, criteria); expect(results).toHaveLength(testFeatures.length); results.forEach(result => { expect(result.breakdown.category).toMatch(/must-have|should-have|could-have|wont-have/); // Must-haves should have highest scores if (result.breakdown.category === 'must-have') { expect(result.score).toBe(100); } else if (result.breakdown.category === 'should-have') { expect(result.score).toBe(75); } else if (result.breakdown.category === 'could-have') { expect(result.score).toBe(50); } else if (result.breakdown.category === 'wont-have') { expect(result.score).toBe(25); } }); }); }); describe('Kano Model', () => { it('should prioritize features using Kano model', async () => { // Add features with keywords that trigger Kano categories const kanoFeatures: Feature[] = [ ...testFeatures, { id: 'feature-security', initiativeId: 'init-1', name: 'Security Enhancement', description: 'Improve security and reliability of the system', userStories: [], priority: 0, businessValue: { score: 70, rationale: 'Essential', metrics: [] }, technicalComplexity: 'medium', status: 'proposed' }, { id: 'feature-ai', initiativeId: 'init-1', name: 'AI-Powered Feature', description: 'Innovative AI capabilities for users', userStories: [], priority: 0, businessValue: { score: 80, rationale: 'Differentiator', metrics: [] }, technicalComplexity: 'high', status: 'proposed' } ]; const criteria: PrioritizationCriteria = { method: 'kano' }; const results = await engine.prioritizeFeatures(kanoFeatures, criteria); // Security features should be categorized as basic const securityFeature = results.find(r => r.featureId === 'feature-security'); expect(securityFeature!.breakdown.category).toBe('basic'); // AI features should be categorized as excitement const aiFeature = results.find(r => r.featureId === 'feature-ai'); expect(aiFeature!.breakdown.category).toBe('excitement'); expect(aiFeature!.score).toBe(100); // Excitement features get highest score }); }); describe('Custom Weighted Scoring', () => { it('should prioritize features using custom weights', async () => { const criteria: PrioritizationCriteria = { method: 'custom', weights: { businessValue: 3, userImpact: 2, strategicAlignment: 2, technicalFeasibility: 1, risk: 1 } }; const results = await engine.prioritizeFeatures(testFeatures, criteria); expect(results).toHaveLength(testFeatures.length); results.forEach(result => { expect(result.method).toBe('custom'); expect(result.breakdown.weights).toEqual(criteria.weights); expect(result.breakdown.businessValue).toBeGreaterThanOrEqual(0); expect(result.breakdown.businessValue).toBeLessThanOrEqual(100); }); // Features with high business value should rank higher given the weights const highValueFeature = results.find(r => r.featureId === 'feature-1'); const lowValueFeature = results.find(r => r.featureId === 'feature-4'); expect(highValueFeature!.rank).toBeLessThan(lowValueFeature!.rank); }); it('should handle missing weights gracefully', async () => { const criteria: PrioritizationCriteria = { method: 'custom' // No weights provided - should use defaults or throw error }; // This should throw an error or use default weights await expect(engine.prioritizeFeatures(testFeatures, criteria)) .rejects.toThrow(); }); }); describe('Initiative Prioritization', () => { it('should prioritize initiatives', async () => { const initiatives: Initiative[] = [ { id: 'init-1', themeId: 'theme-1', title: 'High Value Initiative', description: 'Strategic initiative with high impact', features: [], epicIds: [], value: { userImpact: 'high', revenueImpact: 1000000, costSavings: 500000, strategicValue: 9, customerSatisfaction: 20 }, effort: { developmentWeeks: 20, designWeeks: 5, qaWeeks: 5, confidence: 'high' }, risks: [], dependencies: [], status: 'validated' }, { id: 'init-2', themeId: 'theme-1', title: 'Low Value Initiative', description: 'Minor improvement initiative', features: [], epicIds: [], value: { userImpact: 'low', revenueImpact: 50000, costSavings: 10000, strategicValue: 3, customerSatisfaction: 2 }, effort: { developmentWeeks: 4, designWeeks: 1, qaWeeks: 1, confidence: 'medium' }, risks: [], dependencies: [], status: 'ideation' } ]; const criteria: PrioritizationCriteria = { method: 'value-effort' }; const results = await engine.prioritizeInitiatives(initiatives, criteria); expect(results).toHaveLength(initiatives.length); // High value initiative should rank higher despite higher effort const highValue = results.find(r => r.featureId === 'init-1'); const lowValue = results.find(r => r.featureId === 'init-2'); // Since the low value initiative has much lower effort, it might rank higher // Just check that both are ranked expect(highValue!.rank).toBeGreaterThanOrEqual(1); expect(highValue!.rank).toBeLessThanOrEqual(2); expect(lowValue!.rank).toBeGreaterThanOrEqual(1); expect(lowValue!.rank).toBeLessThanOrEqual(2); }); }); describe('Edge Cases', () => { it('should handle empty feature list', async () => { const criteria: PrioritizationCriteria = { method: 'rice' }; const results = await engine.prioritizeFeatures([], criteria); expect(results).toEqual([]); }); it('should handle single feature', async () => { const criteria: PrioritizationCriteria = { method: 'rice' }; const results = await engine.prioritizeFeatures([testFeatures[0]], criteria); expect(results).toHaveLength(1); expect(results[0].rank).toBe(1); }); it('should throw error for unknown method', async () => { const criteria: PrioritizationCriteria = { method: 'unknown-method' as any }; await expect(engine.prioritizeFeatures(testFeatures, criteria)) .rejects.toThrow('Unknown prioritization method'); }); }); });