UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

291 lines (247 loc) 8.86 kB
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'); }); }); });