@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
389 lines (344 loc) • 12.4 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { RoadmapManager } from '../roadmap-manager.js';
import { ConfigManager } from '../../../config/config-manager.js';
import {
CreateRoadmapOptions,
AddThemeOptions,
CreateInitiativeOptions,
AddFeatureOptions,
ProductRoadmap,
RoadmapTheme,
Initiative,
Feature
} from '../types.js';
// Mock the storage manager
vi.mock('../../../storage/storage-manager.js', () => ({
StorageManager: vi.fn().mockImplementation(() => ({
getStorageLocation: vi.fn().mockResolvedValue({
data: '/tmp/test-atlas'
})
}))
}));
// Mock fs operations
vi.mock('fs', () => ({
promises: {
access: vi.fn().mockRejectedValue(new Error('File not found')),
mkdir: vi.fn().mockResolvedValue(undefined),
readFile: vi.fn().mockRejectedValue(new Error('File not found')),
writeFile: vi.fn().mockResolvedValue(undefined)
}
}));
describe('RoadmapManager', () => {
let roadmapManager: RoadmapManager;
let configManager: ConfigManager;
beforeEach(async () => {
configManager = new ConfigManager();
roadmapManager = new RoadmapManager(configManager);
await roadmapManager.initialize();
});
describe('createRoadmap', () => {
it('should create a new product roadmap', async () => {
const options: CreateRoadmapOptions = {
name: 'Test Product Roadmap',
vision: 'To create an amazing product that users love',
timeHorizon: 'annual',
owner: 'John Doe',
stakeholders: ['Jane Smith', 'Bob Johnson']
};
const roadmap = await roadmapManager.createRoadmap(options);
expect(roadmap).toBeDefined();
expect(roadmap.name).toBe(options.name);
expect(roadmap.vision).toBe(options.vision);
expect(roadmap.timeHorizon).toBe(options.timeHorizon);
expect(roadmap.owner).toBe(options.owner);
expect(roadmap.stakeholders).toEqual(options.stakeholders);
expect(roadmap.status).toBe('draft');
expect(roadmap.themes).toEqual([]);
expect(roadmap.milestones).toEqual([]);
expect(roadmap.releases).toEqual([]);
expect(roadmap.metrics.featuresPlanned).toBe(0);
expect(roadmap.metrics.featuresCompleted).toBe(0);
});
it('should generate unique IDs for roadmaps', async () => {
const options: CreateRoadmapOptions = {
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'quarterly',
owner: 'Test Owner'
};
const roadmap1 = await roadmapManager.createRoadmap(options);
const roadmap2 = await roadmapManager.createRoadmap(options);
expect(roadmap1.id).not.toBe(roadmap2.id);
});
});
describe('addTheme', () => {
let roadmap: ProductRoadmap;
beforeEach(async () => {
roadmap = await roadmapManager.createRoadmap({
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'annual',
owner: 'Test Owner'
});
});
it('should add a theme to a roadmap', async () => {
const themeOptions: AddThemeOptions = {
roadmapId: roadmap.id,
name: 'User Experience Enhancement',
description: 'Improve overall user experience across the platform',
objectives: [
'Reduce user friction',
'Improve onboarding flow',
'Enhance mobile experience'
],
priority: 'must-have',
startQuarter: 'Q1 2024',
endQuarter: 'Q4 2024'
};
const theme = await roadmapManager.addTheme(themeOptions);
expect(theme).toBeDefined();
expect(theme.name).toBe(themeOptions.name);
expect(theme.description).toBe(themeOptions.description);
expect(theme.objectives).toEqual(themeOptions.objectives);
expect(theme.priority).toBe(themeOptions.priority);
expect(theme.timeframe.startQuarter).toBe(themeOptions.startQuarter);
expect(theme.timeframe.endQuarter).toBe(themeOptions.endQuarter);
expect(theme.status).toBe('planned');
expect(theme.initiatives).toEqual([]);
expect(theme.metrics.initiativesTotal).toBe(0);
});
it('should throw error for non-existent roadmap', async () => {
const themeOptions: AddThemeOptions = {
roadmapId: 'non-existent-roadmap',
name: 'Test Theme',
description: 'Test description',
objectives: ['Test objective'],
priority: 'nice-to-have',
startQuarter: 'Q1 2024',
endQuarter: 'Q2 2024'
};
await expect(roadmapManager.addTheme(themeOptions))
.rejects.toThrow('Roadmap non-existent-roadmap not found');
});
});
describe('createInitiative', () => {
let roadmap: ProductRoadmap;
let theme: RoadmapTheme;
beforeEach(async () => {
roadmap = await roadmapManager.createRoadmap({
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'annual',
owner: 'Test Owner'
});
theme = await roadmapManager.addTheme({
roadmapId: roadmap.id,
name: 'Test Theme',
description: 'Test description',
objectives: ['Test objective'],
priority: 'must-have',
startQuarter: 'Q1 2024',
endQuarter: 'Q4 2024'
});
});
it('should create an initiative within a theme', async () => {
const initiativeOptions: CreateInitiativeOptions = {
roadmapId: roadmap.id,
themeId: theme.id,
title: 'Implement Smart Search',
description: 'Build an AI-powered search system',
estimatedValue: {
userImpact: 'high',
revenueImpact: 500000,
costSavings: 200000,
strategicValue: 8,
customerSatisfaction: 15
},
estimatedEffort: {
developmentWeeks: 12,
designWeeks: 4,
qaWeeks: 3,
confidence: 'medium'
},
risks: [{
id: 'risk-1',
description: 'Technical complexity',
likelihood: 'medium',
impact: 'high',
mitigation: 'Conduct technical spike'
}]
};
const initiative = await roadmapManager.createInitiative(initiativeOptions);
expect(initiative).toBeDefined();
expect(initiative.themeId).toBe(theme.id);
expect(initiative.title).toBe(initiativeOptions.title);
expect(initiative.description).toBe(initiativeOptions.description);
expect(initiative.value).toEqual(initiativeOptions.estimatedValue);
expect(initiative.effort).toEqual(initiativeOptions.estimatedEffort);
expect(initiative.risks).toHaveLength(1);
expect(initiative.status).toBe('ideation');
expect(initiative.features).toEqual([]);
expect(initiative.epicIds).toEqual([]);
});
});
describe('addFeature', () => {
let roadmap: ProductRoadmap;
let theme: RoadmapTheme;
let initiative: Initiative;
beforeEach(async () => {
roadmap = await roadmapManager.createRoadmap({
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'annual',
owner: 'Test Owner'
});
theme = await roadmapManager.addTheme({
roadmapId: roadmap.id,
name: 'Test Theme',
description: 'Test description',
objectives: ['Test objective'],
priority: 'must-have',
startQuarter: 'Q1 2024',
endQuarter: 'Q4 2024'
});
initiative = await roadmapManager.createInitiative({
roadmapId: roadmap.id,
themeId: theme.id,
title: 'Test Initiative',
description: 'Test initiative description',
estimatedValue: {
userImpact: 'medium',
revenueImpact: 100000,
costSavings: 50000,
strategicValue: 7,
customerSatisfaction: 10
},
estimatedEffort: {
developmentWeeks: 8,
designWeeks: 2,
qaWeeks: 2,
confidence: 'high'
}
});
});
it('should add a feature to an initiative', async () => {
const featureOptions: AddFeatureOptions = {
roadmapId: roadmap.id,
initiativeId: initiative.id,
name: 'Natural Language Search',
description: 'Allow users to search using natural language queries',
businessValue: {
score: 85,
rationale: 'High user demand and competitive advantage',
metrics: ['Search success rate > 90%', 'User satisfaction > 4.5/5']
},
technicalComplexity: 'high',
targetRelease: 'v2.0'
};
const feature = await roadmapManager.addFeature(featureOptions);
expect(feature).toBeDefined();
expect(feature.initiativeId).toBe(initiative.id);
expect(feature.name).toBe(featureOptions.name);
expect(feature.description).toBe(featureOptions.description);
expect(feature.businessValue).toEqual(featureOptions.businessValue);
expect(feature.technicalComplexity).toBe(featureOptions.technicalComplexity);
expect(feature.targetRelease).toBe(featureOptions.targetRelease);
expect(feature.status).toBe('proposed');
expect(feature.priority).toBe(0);
});
});
describe('updateFeatureStatus', () => {
let feature: Feature;
beforeEach(async () => {
const roadmap = await roadmapManager.createRoadmap({
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'annual',
owner: 'Test Owner'
});
const theme = await roadmapManager.addTheme({
roadmapId: roadmap.id,
name: 'Test Theme',
description: 'Test description',
objectives: ['Test objective'],
priority: 'must-have',
startQuarter: 'Q1 2024',
endQuarter: 'Q4 2024'
});
const initiative = await roadmapManager.createInitiative({
roadmapId: roadmap.id,
themeId: theme.id,
title: 'Test Initiative',
description: 'Test initiative',
estimatedValue: {
userImpact: 'medium',
revenueImpact: 100000,
costSavings: 50000,
strategicValue: 7,
customerSatisfaction: 10
},
estimatedEffort: {
developmentWeeks: 8,
designWeeks: 2,
qaWeeks: 2,
confidence: 'high'
}
});
feature = await roadmapManager.addFeature({
roadmapId: roadmap.id,
initiativeId: initiative.id,
name: 'Test Feature',
description: 'Test feature description',
businessValue: {
score: 75,
rationale: 'Test rationale',
metrics: ['Test metric']
},
technicalComplexity: 'medium'
});
});
it('should update feature status', async () => {
const updatedFeature = await roadmapManager.updateFeatureStatus(
feature.id,
'in-progress'
);
expect(updatedFeature.status).toBe('in-progress');
});
it('should throw error for non-existent feature', async () => {
await expect(roadmapManager.updateFeatureStatus('non-existent', 'completed'))
.rejects.toThrow('Feature non-existent not found');
});
});
describe('getRoadmapHealth', () => {
let roadmap: ProductRoadmap;
beforeEach(async () => {
roadmap = await roadmapManager.createRoadmap({
name: 'Test Roadmap',
vision: 'Test vision',
timeHorizon: 'annual',
owner: 'Test Owner'
});
});
it('should return health assessment for a roadmap', async () => {
const health = await roadmapManager.getRoadmapHealth(roadmap.id);
expect(health).toBeDefined();
expect(health.health).toMatch(/excellent|good|at-risk|critical/);
expect(health.metrics).toBeDefined();
expect(health.risks).toBeInstanceOf(Array);
expect(health.recommendations).toBeInstanceOf(Array);
});
it('should identify risks for low on-time delivery', async () => {
// This would require mocking the roadmap metrics
// For now, we just test the structure
const health = await roadmapManager.getRoadmapHealth(roadmap.id);
expect(health.metrics.onTimeDelivery).toBeDefined();
expect(health.metrics.velocityTrend).toBeDefined();
});
});
describe('getThemeDetails', () => {
it('should return null for non-existent theme', async () => {
const details = await roadmapManager.getThemeDetails('non-existent');
expect(details).toBeNull();
});
});
});