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