@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
291 lines (247 loc) • 8.86 kB
text/typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { randomUUID } from 'crypto';
import { setupAgileManagement } from '../index';
import type { Server } from '@modelcontextprotocol/sdk/types.js';
import type { RequestContext } from '../../../core/types';
describe('Workflow Validation', () => {
let mockServer: Server;
let mockContext: RequestContext;
let tools: any;
let mockDb: any;
beforeEach(async () => {
// Mock server
mockServer = {
setRequestHandler: vi.fn()
} as any;
// Mock database
mockDb = {
run: vi.fn(),
get: vi.fn(),
query: vi.fn()
};
// Mock context
mockContext = {
projectId: 'test-project',
userId: 'test-user',
db: mockDb
} as any;
// Setup tools
const setup = await setupAgileManagement(mockServer);
tools = setup.tools.reduce((acc, tool) => {
acc[tool.name] = tool;
return acc;
}, {} as any);
});
describe('Todo to In-Progress Workflow', () => {
it('should allow transition from todo to in_progress', async () => {
// Mock story in todo status
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'todo' }
});
mockDb.run.mockResolvedValue({
success: true,
data: { changes: 1 }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress',
notes: 'Starting development'
}, mockContext);
expect(result.success).toBe(true);
expect(result.data.oldStatus).toBe('todo');
expect(result.data.newStatus).toBe('in_progress');
expect(result.data.workflowValidationBypassed).toBe(false);
});
it('should block transition from review to in_progress', async () => {
// Mock story in review status
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'review' }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress',
notes: 'Going back to development'
}, mockContext);
expect(result.success).toBe(false);
expect(result.error.code).toBe('WORKFLOW_VALIDATION_ERROR');
expect(result.error.message).toContain('Cannot transition from "review" to "in_progress"');
expect(result.error.details.suggestedAction).toContain('Stories must be in "todo" status');
});
it('should allow transition from done to in_progress with workflow validation bypass', async () => {
// Mock story in done status
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'done' }
});
mockDb.run.mockResolvedValue({
success: true,
data: { changes: 1 }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress',
notes: 'Reopening for more work',
skipWorkflowValidation: true
}, mockContext);
expect(result.success).toBe(true);
expect(result.data.workflowValidationBypassed).toBe(true);
// Should log bypass in history
expect(mockDb.run).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO story_history'),
expect.arrayContaining([
expect.any(String), // id
'story-1',
'status_change',
'done',
'in_progress',
'Reopening for more work [WORKFLOW VALIDATION BYPASSED]',
'test-user',
expect.any(Number)
])
);
});
});
describe('Other Valid Transitions', () => {
it('should allow standard workflow transitions', async () => {
const validTransitions = [
{ from: 'todo', to: 'in_progress' },
{ from: 'in_progress', to: 'review' },
{ from: 'review', to: 'done' },
{ from: 'done', to: 'review' }, // Can reopen
{ from: 'blocked', to: 'todo' },
{ from: 'blocked', to: 'in_progress' }
];
for (const transition of validTransitions) {
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: transition.from }
});
mockDb.run.mockResolvedValue({
success: true,
data: { changes: 1 }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: transition.to,
notes: `Moving from ${transition.from} to ${transition.to}`
}, mockContext);
if (!result.success) {
console.error(`Failed transition: ${transition.from} -> ${transition.to}`, result.error);
}
expect(result.success).toBe(true,
`Transition from ${transition.from} to ${transition.to} should be valid`);
}
});
});
describe('Invalid Transitions', () => {
it('should block invalid transitions', async () => {
const invalidTransitions = [
{ from: 'todo', to: 'review' },
{ from: 'todo', to: 'done' },
{ from: 'in_progress', to: 'todo' },
{ from: 'review', to: 'todo' }
];
for (const transition of invalidTransitions) {
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: transition.from }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: transition.to,
notes: `Trying invalid transition`
}, mockContext);
expect(result.success).toBe(false,
`Transition from ${transition.from} to ${transition.to} should be blocked`);
expect(result.error.code).toBe('WORKFLOW_VALIDATION_ERROR');
}
});
});
describe('Admin Override', () => {
it('should allow any transition with skipWorkflowValidation', async () => {
// Test the most restricted case: todo -> in_progress
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'todo' }
});
mockDb.run.mockResolvedValue({
success: true,
data: { changes: 1 }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress',
notes: 'Emergency fix, bypassing workflow',
skipWorkflowValidation: true
}, mockContext);
expect(result.success).toBe(true);
expect(result.data.workflowValidationBypassed).toBe(true);
// Should log the bypass
expect(mockDb.run).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO story_history'),
expect.arrayContaining([
expect.any(String),
'story-1',
'status_change',
'todo',
'in_progress',
'Emergency fix, bypassing workflow [WORKFLOW VALIDATION BYPASSED]',
'test-user',
expect.any(Number)
])
);
});
});
describe('Same Status Transitions', () => {
it('should allow same status transitions', async () => {
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'in_progress' }
});
mockDb.run.mockResolvedValue({
success: true,
data: { changes: 1 }
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress',
notes: 'Updating with progress notes'
}, mockContext);
expect(result.success).toBe(true);
expect(result.data.oldStatus).toBe('in_progress');
expect(result.data.newStatus).toBe('in_progress');
});
});
describe('Error Cases', () => {
it('should handle story not found', async () => {
mockDb.get.mockResolvedValue({
success: false,
data: null
});
const result = await tools.update_story_status.execute({
storyId: 'nonexistent-story',
status: 'in_progress'
}, mockContext);
expect(result.success).toBe(false);
expect(result.error.code).toBe('RESOURCE_NOT_FOUND');
});
it('should handle database errors', async () => {
mockDb.get.mockResolvedValue({
success: true,
data: { id: 'story-1', title: 'Test Story', status: 'todo' }
});
mockDb.run.mockResolvedValue({
success: false,
error: 'Database connection failed'
});
const result = await tools.update_story_status.execute({
storyId: 'story-1',
status: 'in_progress'
}, mockContext);
expect(result.success).toBe(false);
expect(result.error.code).toBe('DATABASE_ERROR');
});
});
});