@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
372 lines (330 loc) • 12 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setupAgileManagementTools } from '../tools.js';
import { SqliteManager } from '../../../storage/sqlite-manager.js';
// Mock SqliteManager
vi.mock('../../../storage/sqlite-manager.js', () => ({
SqliteManager: {
getInstance: vi.fn()
}
}));
describe('Agile Management Tools', () => {
let tools: any;
let mockDb: vi.Mocked<SqliteManager>;
beforeEach(async () => {
vi.clearAllMocks();
// Mock database operations
mockDb = {
run: vi.fn().mockResolvedValue({ success: true, changes: 1 }),
get: vi.fn().mockResolvedValue({ success: true, data: null }),
query: vi.fn().mockResolvedValue({ success: true, data: [] }),
transaction: vi.fn().mockImplementation(async (callback) => {
return await callback(mockDb);
})
} as any;
vi.mocked(SqliteManager.getInstance).mockReturnValue(mockDb);
// Setup tools
const toolRegistration = await setupAgileManagementTools();
tools = {};
for (const tool of toolRegistration.tools) {
tools[tool.name] = tool;
}
});
const createContext = (toolName: string) => ({
toolName,
requestId: 'test-req-1',
projectId: 'test-project',
userId: 'test-user',
timestamp: Date.now(),
db: mockDb
});
describe('create_agile_sprint', () => {
it('should create a new sprint with default values', async () => {
const input = {
name: 'Sprint 1',
goal: 'Implement user authentication',
duration: 14,
};
const result = await tools.create_agile_sprint.execute(input, createContext('create_agile_sprint'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.sprint).toBeDefined();
expect(result.data.sprint.name).toBe('Sprint 1');
expect(result.data.sprint.goal).toBe('Implement user authentication');
expect(result.data.sprint.duration).toBe(14);
expect(result.data.sprint.status).toBe('active');
expect(result.data.sprint.id).toMatch(/^[a-zA-Z0-9-]+$/);
expect(mockDb.run).toHaveBeenCalled();
});
it('should create sprint with team members', async () => {
const input = {
name: 'Sprint 2',
goal: 'Build dashboard',
duration: 10,
team: ['alice@example.com', 'bob@example.com'],
};
const result = await tools.create_agile_sprint.execute(input, createContext('create_agile_sprint'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.sprint).toBeDefined();
expect(result.data.sprint.team).toEqual(['alice@example.com', 'bob@example.com']);
});
});
describe('create_user_story', () => {
it('should create a new story', async () => {
const input = {
title: 'User login functionality',
description: 'As a user, I want to log in',
priority: 'high' as const,
storyPoints: 5
};
const result = await tools.create_user_story.execute(input, createContext('create_user_story'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.story).toBeDefined();
expect(result.data.story.title).toBe('User login functionality');
expect(result.data.story.description).toBe('As a user, I want to log in');
expect(result.data.story.priority).toBe('high');
expect(result.data.story.storyPoints).toBe(5);
expect(result.data.story.id).toMatch(/^[a-zA-Z0-9-]+$/);
expect(mockDb.run).toHaveBeenCalled();
});
});
describe('update_story_status', () => {
it('should update story status', async () => {
// Mock story check
mockDb.get.mockResolvedValueOnce({
success: true,
data: {
id: 'STORY-0001',
title: 'Test Story',
status: 'todo'
}
});
const result = await tools.update_story_status.execute({
storyId: 'STORY-0001',
status: 'in_progress',
notes: 'Started working on this',
skipWorkflowValidation: true // Skip validation for this test
}, createContext('update_story_status'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.oldStatus).toBe('todo');
expect(result.data.newStatus).toBe('in_progress');
expect(mockDb.run).toHaveBeenCalled();
});
});
describe('get_sprint_stats', () => {
it('should get sprint statistics', async () => {
// Mock sprint data
mockDb.get.mockResolvedValueOnce({
success: true,
data: {
id: 'SPRINT-0001',
name: 'Sprint 1',
goal: 'Test goal',
status: 'active',
duration_days: 14,
start_date: Date.now(),
end_date: Date.now() + (14 * 24 * 60 * 60 * 1000),
team_members: '[]'
}
});
// Mock stats data
mockDb.query.mockResolvedValueOnce({
success: true,
data: [{
total_stories: 5,
completed_stories: 2,
in_progress_stories: 1,
review_stories: 1,
todo_stories: 1,
total_points: 20,
completed_points: 8
}]
});
const result = await tools.get_sprint_stats.execute({ sprintId: 'SPRINT-0001' }, createContext('get_sprint_stats'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.sprint).toBeDefined();
expect(result.data.sprint.name).toBe('Sprint 1');
expect(result.data.progress).toBeDefined();
expect(result.data.progress.stories).toBeDefined();
expect(result.data.progress.stories.total).toBe(5);
expect(mockDb.get).toHaveBeenCalled();
expect(mockDb.query).toHaveBeenCalled();
});
});
describe('list_stories', () => {
it('should list stories with no filters', async () => {
mockDb.query.mockResolvedValueOnce({
success: true,
data: [
{
id: 'STORY-0001',
title: 'Test Story',
description: 'Test description',
status: 'todo',
priority: 'medium',
story_points: 3,
acceptance_criteria: '[]',
tags: '[]',
assigned_to: null,
sprint_id: null,
epic_id: null,
created_at: Date.now(),
updated_at: Date.now()
}
]
});
const result = await tools.list_stories.execute({}, createContext('list_stories'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.stories).toBeDefined();
expect(result.data.stories).toHaveLength(1);
expect(result.data.stories[0].title).toBe('Test Story');
expect(mockDb.query).toHaveBeenCalled();
});
});
describe('add_story_to_sprint', () => {
it('should add story to sprint', async () => {
// Mock story check
mockDb.get.mockResolvedValueOnce({
success: true,
data: {
id: 'STORY-0001',
title: 'Test Story',
sprint_id: null
}
});
// Mock sprint check
mockDb.get.mockResolvedValueOnce({
success: true,
data: {
id: 'SPRINT-0001',
name: 'Sprint 1',
status: 'planning'
}
});
const result = await tools.add_story_to_sprint.execute({
storyId: 'STORY-0001',
sprintId: 'SPRINT-0001'
}, createContext('add_story_to_sprint'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.message).toContain('Test Story');
expect(result.data.message).toContain('Sprint 1');
expect(mockDb.run).toHaveBeenCalled();
});
});
describe('list_stories_with_details', () => {
it('should list stories with documentation fields', async () => {
mockDb.query.mockResolvedValueOnce({
success: true,
data: [
{
id: 'STORY-0001',
title: 'Test Story with Docs',
description: 'Test description',
status: 'in_progress',
priority: 'high',
story_points: 5,
implementation_doc_url: 'https://docs.example.com/impl',
design_doc_url: 'https://docs.example.com/design',
documentation_status: 'approved',
acceptance_criteria: '[]',
tags: '[]',
assigned_to: 'john@example.com',
sprint_id: 'SPRINT-0001',
epic_id: null,
created_at: Date.now(),
updated_at: Date.now()
},
{
id: 'STORY-0002',
title: 'Test Story without Docs',
description: 'Another test',
status: 'todo',
priority: 'medium',
story_points: 3,
implementation_doc_url: null,
design_doc_url: null,
documentation_status: null,
acceptance_criteria: '[]',
tags: '[]',
assigned_to: null,
sprint_id: 'SPRINT-0001',
epic_id: null,
created_at: Date.now(),
updated_at: Date.now()
}
]
});
const result = await tools.list_stories_with_details.execute({
sprintId: 'SPRINT-0001',
includeFields: ['id', 'title', 'status', 'implementationDocumentUrl', 'designDocumentUrl', 'documentationStatus']
}, createContext('list_stories_with_details'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.stories).toBeDefined();
expect(result.data.stories).toHaveLength(2);
expect(result.data.stories[0].implementationDocumentUrl).toBe('https://docs.example.com/impl');
expect(result.data.stories[1].implementationDocumentUrl).toBeNull();
expect(result.data.summary).toBeDefined();
expect(result.data.summary.missingDocumentation).toBe(1);
expect(mockDb.query).toHaveBeenCalled();
});
it('should filter by status', async () => {
mockDb.query.mockResolvedValueOnce({
success: true,
data: [
{
id: 'STORY-0001',
title: 'In Progress Story',
status: 'in_progress',
story_points: 5,
sprint_id: 'SPRINT-0001'
}
]
});
const result = await tools.list_stories_with_details.execute({
status: 'in_progress',
includeFields: ['id', 'title', 'status']
}, createContext('list_stories_with_details'));
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.stories).toBeDefined();
expect(result.data.stories).toHaveLength(1);
expect(result.data.stories[0].status).toBe('in_progress');
});
});
describe('tool registration', () => {
it('should register all expected tools', () => {
const expectedTools = [
'create_agile_sprint',
'create_user_story',
'add_story_to_sprint',
'update_story_status',
'get_sprint_stats',
'list_stories',
'list_stories_with_details'
];
for (const toolName of expectedTools) {
expect(tools[toolName]).toBeDefined();
expect(typeof tools[toolName].execute).toBe('function');
}
});
});
describe('error handling', () => {
it('should handle database errors gracefully', async () => {
mockDb.run.mockResolvedValueOnce({ success: false, error: 'Database error' });
const input = {
name: 'Sprint 1',
goal: 'Test goal',
duration: 14,
};
const result = await tools.create_agile_sprint.execute(input, createContext('create_agile_sprint'));
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
});