Skip to main content
Glama
epic-service.test.ts21.8 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { EpicService, CreateEpicParams, UpdateEpicParams, EpicQueryParams, getEpicService } from '../../services/epic-service.js'; import { Epic, AtomicTask, TaskStatus, TaskPriority, TaskType } from '../../types/task.js'; // Mock storage manager vi.mock('../../core/storage/storage-manager.js', () => ({ getStorageManager: vi.fn() })); // Mock task operations vi.mock('../../core/operations/task-operations.js', () => ({ getTaskOperations: vi.fn() })); // Mock ID generator vi.mock('../../utils/id-generator.js', () => ({ getIdGenerator: vi.fn() })); // Mock logger vi.mock('../../../../logger.js', () => ({ default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() } })); describe('EpicService', () => { let epicService: EpicService; let mockStorageManager: Record<string, unknown>; let mockTaskOperations: Record<string, unknown>; let mockIdGenerator: Record<string, unknown>; const mockEpic: Epic = { id: 'E001', title: 'Test Epic', description: 'Complete user authentication system', status: 'pending', priority: 'high', projectId: 'PID-TEST-001', estimatedHours: 40, taskIds: ['T001', 'T002'], dependencies: [], dependents: [], metadata: { createdAt: new Date('2024-01-20'), updatedAt: new Date('2024-01-20'), createdBy: 'test-user', tags: ['authentication', 'security'] } }; const mockTask: AtomicTask = { id: 'T001', title: 'Implement login form', description: 'Create login form component', type: 'development' as TaskType, priority: 'high' as TaskPriority, status: 'pending' as TaskStatus, projectId: 'PID-TEST-001', epicId: 'E001', estimatedHours: 4, actualHours: 0, filePaths: ['src/components/LoginForm.tsx'], acceptanceCriteria: ['Form validates input', 'Form submits correctly'], tags: ['frontend', 'authentication'], dependencies: [], assignedAgent: null, createdAt: new Date('2024-01-20'), updatedAt: new Date('2024-01-20'), createdBy: 'test-user' }; beforeEach(async () => { // Clear singleton instance (EpicService as unknown as { instance: unknown }).instance = undefined; epicService = EpicService.getInstance(); // Setup mocks const storageModule = await import('../../core/storage/storage-manager.js'); const taskModule = await import('../../core/operations/task-operations.js'); const idModule = await import('../../utils/id-generator.js'); mockStorageManager = { projectExists: vi.fn(), createEpic: vi.fn(), getEpic: vi.fn(), updateEpic: vi.fn(), deleteEpic: vi.fn(), listEpics: vi.fn() }; mockTaskOperations = { getTask: vi.fn() }; mockIdGenerator = { generateEpicId: vi.fn() }; vi.mocked(storageModule.getStorageManager).mockResolvedValue(mockStorageManager); vi.mocked(taskModule.getTaskOperations).mockReturnValue(mockTaskOperations); vi.mocked(idModule.getIdGenerator).mockReturnValue(mockIdGenerator); }); afterEach(() => { vi.clearAllMocks(); }); describe('singleton pattern', () => { it('should return the same instance', () => { const instance1 = EpicService.getInstance(); const instance2 = EpicService.getInstance(); expect(instance1).toBe(instance2); }); it('should work with convenience function', () => { const instance1 = getEpicService(); const instance2 = EpicService.getInstance(); expect(instance1).toBe(instance2); }); }); describe('createEpic', () => { const createParams: CreateEpicParams = { title: 'Test Epic', description: 'Test epic description', projectId: 'PID-TEST-001', priority: 'high', estimatedHours: 40, tags: ['test'], dependencies: [] }; it('should create epic successfully', async () => { mockStorageManager.projectExists.mockResolvedValue(true); mockIdGenerator.generateEpicId.mockResolvedValue({ success: true, id: 'E001' }); mockStorageManager.createEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'create', timestamp: new Date() } }); const result = await epicService.createEpic(createParams, 'test-user'); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data!.title).toBe('Test Epic'); expect(mockStorageManager.projectExists).toHaveBeenCalledWith('PID-TEST-001'); expect(mockIdGenerator.generateEpicId).toHaveBeenCalledWith('PID-TEST-001'); expect(mockStorageManager.createEpic).toHaveBeenCalled(); }); it('should fail when project does not exist', async () => { mockStorageManager.projectExists.mockResolvedValue(false); const result = await epicService.createEpic(createParams); expect(result.success).toBe(false); expect(result.error).toContain('Project PID-TEST-001 not found'); }); it('should fail with invalid parameters', async () => { const invalidParams = { title: '', // Invalid: empty title description: 'Valid description', projectId: 'PID-TEST-001' }; const result = await epicService.createEpic(invalidParams); expect(result.success).toBe(false); expect(result.error).toContain('validation failed'); }); it('should fail when ID generation fails', async () => { mockStorageManager.projectExists.mockResolvedValue(true); mockIdGenerator.generateEpicId.mockResolvedValue({ success: false, error: 'ID generation failed' }); const result = await epicService.createEpic(createParams); expect(result.success).toBe(false); expect(result.error).toContain('Failed to generate epic ID'); }); it('should fail when storage creation fails', async () => { mockStorageManager.projectExists.mockResolvedValue(true); mockIdGenerator.generateEpicId.mockResolvedValue({ success: true, id: 'E001' }); mockStorageManager.createEpic.mockResolvedValue({ success: false, error: 'Storage error', metadata: { filePath: 'test', operation: 'create', timestamp: new Date() } }); const result = await epicService.createEpic(createParams); expect(result.success).toBe(false); expect(result.error).toContain('Failed to save epic'); }); }); describe('getEpic', () => { it('should get epic successfully', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.getEpic('E001'); expect(result.success).toBe(true); expect(result.data).toEqual(mockEpic); expect(mockStorageManager.getEpic).toHaveBeenCalledWith('E001'); }); it('should fail when epic not found', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: false, error: 'Epic not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.getEpic('E999'); expect(result.success).toBe(false); expect(result.error).toContain('Epic not found'); }); }); describe('updateEpic', () => { const updateParams: UpdateEpicParams = { title: 'Updated Epic Title', status: 'in_progress', priority: 'critical' }; it('should update epic successfully', async () => { const updatedEpic = { ...mockEpic, ...updateParams }; mockStorageManager.updateEpic.mockResolvedValue({ success: true, data: updatedEpic, metadata: { filePath: 'test', operation: 'update', timestamp: new Date() } }); const result = await epicService.updateEpic('E001', updateParams, 'test-user'); expect(result.success).toBe(true); expect(result.data!.title).toBe(updateParams.title); expect(result.data!.status).toBe(updateParams.status); expect(mockStorageManager.updateEpic).toHaveBeenCalledWith('E001', expect.objectContaining(updateParams)); }); it('should fail with invalid update parameters', async () => { const invalidParams = { title: '', // Invalid: empty title status: 'invalid_status' as TaskStatus // Invalid status }; const result = await epicService.updateEpic('E001', invalidParams); expect(result.success).toBe(false); expect(result.error).toContain('validation failed'); }); it('should fail when storage update fails', async () => { mockStorageManager.updateEpic.mockResolvedValue({ success: false, error: 'Update failed', metadata: { filePath: 'test', operation: 'update', timestamp: new Date() } }); const result = await epicService.updateEpic('E001', updateParams); expect(result.success).toBe(false); expect(result.error).toContain('Failed to update epic'); }); }); describe('deleteEpic', () => { it('should delete epic successfully', async () => { mockStorageManager.deleteEpic.mockResolvedValue({ success: true, metadata: { filePath: 'test', operation: 'delete', timestamp: new Date() } }); const result = await epicService.deleteEpic('E001', 'test-user'); expect(result.success).toBe(true); expect(mockStorageManager.deleteEpic).toHaveBeenCalledWith('E001'); }); it('should fail when storage deletion fails', async () => { mockStorageManager.deleteEpic.mockResolvedValue({ success: false, error: 'Delete failed', metadata: { filePath: 'test', operation: 'delete', timestamp: new Date() } }); const result = await epicService.deleteEpic('E001'); expect(result.success).toBe(false); expect(result.error).toContain('Failed to delete epic'); }); }); describe('listEpics', () => { const mockEpics = [mockEpic, { ...mockEpic, id: 'E002', title: 'Another Epic' }]; it('should list epics successfully', async () => { mockStorageManager.listEpics.mockResolvedValue({ success: true, data: mockEpics, metadata: { filePath: 'test', operation: 'list', timestamp: new Date() } }); const result = await epicService.listEpics(); expect(result.success).toBe(true); expect(result.data).toHaveLength(2); expect(mockStorageManager.listEpics).toHaveBeenCalledWith(undefined); }); it('should list epics with project filter', async () => { mockStorageManager.listEpics.mockResolvedValue({ success: true, data: mockEpics, metadata: { filePath: 'test', operation: 'list', timestamp: new Date() } }); const query: EpicQueryParams = { projectId: 'PID-TEST-001' }; const result = await epicService.listEpics(query); expect(result.success).toBe(true); expect(mockStorageManager.listEpics).toHaveBeenCalledWith('PID-TEST-001'); }); it('should apply additional filters', async () => { const epicsWithDifferentStatus = [ { ...mockEpic, status: 'pending' as TaskStatus }, { ...mockEpic, id: 'E002', status: 'completed' as TaskStatus } ]; mockStorageManager.listEpics.mockResolvedValue({ success: true, data: epicsWithDifferentStatus, metadata: { filePath: 'test', operation: 'list', timestamp: new Date() } }); const query: EpicQueryParams = { status: 'pending' }; const result = await epicService.listEpics(query); expect(result.success).toBe(true); expect(result.data).toHaveLength(1); expect(result.data![0].status).toBe('pending'); }); it('should fail when storage listing fails', async () => { mockStorageManager.listEpics.mockResolvedValue({ success: false, error: 'List failed', metadata: { filePath: 'test', operation: 'list', timestamp: new Date() } }); const result = await epicService.listEpics(); expect(result.success).toBe(false); expect(result.error).toContain('List failed'); }); }); describe('addTaskToEpic', () => { it('should add task to epic successfully', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); mockTaskOperations.getTask.mockResolvedValue({ success: true, data: mockTask, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const updatedEpic = { ...mockEpic, taskIds: [...mockEpic.taskIds, 'T003'] }; mockStorageManager.updateEpic.mockResolvedValue({ success: true, data: updatedEpic, metadata: { filePath: 'test', operation: 'update', timestamp: new Date() } }); const result = await epicService.addTaskToEpic('E001', 'T003'); expect(result.success).toBe(true); expect(mockStorageManager.getEpic).toHaveBeenCalledWith('E001'); expect(mockTaskOperations.getTask).toHaveBeenCalledWith('T003'); }); it('should fail when epic not found', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: false, error: 'Epic not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.addTaskToEpic('E999', 'T003'); expect(result.success).toBe(false); expect(result.error).toContain('Epic not found'); }); it('should fail when task not found', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); mockTaskOperations.getTask.mockResolvedValue({ success: false, error: 'Task not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.addTaskToEpic('E001', 'T999'); expect(result.success).toBe(false); expect(result.error).toContain('Task not found'); }); it('should fail when task already in epic', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.addTaskToEpic('E001', 'T001'); // T001 already in epic expect(result.success).toBe(false); expect(result.error).toContain('Task T001 is already in epic E001'); }); }); describe('removeTaskFromEpic', () => { it('should remove task from epic successfully', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const updatedEpic = { ...mockEpic, taskIds: ['T002'] }; // T001 removed mockStorageManager.updateEpic.mockResolvedValue({ success: true, data: updatedEpic, metadata: { filePath: 'test', operation: 'update', timestamp: new Date() } }); const result = await epicService.removeTaskFromEpic('E001', 'T001'); expect(result.success).toBe(true); expect(mockStorageManager.getEpic).toHaveBeenCalledWith('E001'); expect(mockStorageManager.updateEpic).toHaveBeenCalled(); }); it('should fail when epic not found', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: false, error: 'Epic not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.removeTaskFromEpic('E999', 'T001'); expect(result.success).toBe(false); expect(result.error).toContain('Epic not found'); }); it('should fail when task not in epic', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: true, data: mockEpic, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.removeTaskFromEpic('E001', 'T999'); // T999 not in epic expect(result.success).toBe(false); expect(result.error).toContain('Task T999 is not in epic E001'); }); }); describe('getEpicProgress', () => { it('should calculate epic progress correctly', async () => { const tasksInEpic = [ { ...mockTask, id: 'T001', status: 'completed' as TaskStatus, estimatedHours: 4, actualHours: 4 }, { ...mockTask, id: 'T002', status: 'in_progress' as TaskStatus, estimatedHours: 6, actualHours: 2 }, { ...mockTask, id: 'T003', status: 'pending' as TaskStatus, estimatedHours: 8, actualHours: 0 }, { ...mockTask, id: 'T004', status: 'blocked' as TaskStatus, estimatedHours: 4, actualHours: 1 } ]; const epicWithTasks = { ...mockEpic, taskIds: ['T001', 'T002', 'T003', 'T004'] }; mockStorageManager.getEpic.mockResolvedValue({ success: true, data: epicWithTasks, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); tasksInEpic.forEach((task, _index) => { mockTaskOperations.getTask.mockResolvedValueOnce({ success: true, data: task, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); }); const result = await epicService.getEpicProgress('E001'); expect(result.success).toBe(true); expect(result.data!.totalTasks).toBe(4); expect(result.data!.completedTasks).toBe(1); expect(result.data!.inProgressTasks).toBe(1); expect(result.data!.pendingTasks).toBe(1); expect(result.data!.blockedTasks).toBe(1); expect(result.data!.progressPercentage).toBe(25); // 1/4 * 100 expect(result.data!.estimatedHours).toBe(22); // 4+6+8+4 expect(result.data!.actualHours).toBe(7); // 4+2+0+1 expect(result.data!.remainingHours).toBe(15); // 22-7 }); it('should handle epic with no tasks', async () => { const epicWithNoTasks = { ...mockEpic, taskIds: [] }; mockStorageManager.getEpic.mockResolvedValue({ success: true, data: epicWithNoTasks, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.getEpicProgress('E001'); expect(result.success).toBe(true); expect(result.data!.totalTasks).toBe(0); expect(result.data!.progressPercentage).toBe(0); expect(result.data!.estimatedHours).toBe(0); expect(result.data!.actualHours).toBe(0); expect(result.data!.remainingHours).toBe(0); }); it('should fail when epic not found', async () => { mockStorageManager.getEpic.mockResolvedValue({ success: false, error: 'Epic not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.getEpicProgress('E999'); expect(result.success).toBe(false); expect(result.error).toContain('Epic not found'); }); it('should handle missing tasks gracefully', async () => { const epicWithTasks = { ...mockEpic, taskIds: ['T001', 'T999'] }; // T999 doesn't exist mockStorageManager.getEpic.mockResolvedValue({ success: true, data: epicWithTasks, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); mockTaskOperations.getTask .mockResolvedValueOnce({ success: true, data: mockTask, metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }) .mockResolvedValueOnce({ success: false, error: 'Task not found', metadata: { filePath: 'test', operation: 'get', timestamp: new Date() } }); const result = await epicService.getEpicProgress('E001'); expect(result.success).toBe(true); expect(result.data!.totalTasks).toBe(1); // Only T001 found }); }); describe('validation', () => { it('should validate create epic parameters', async () => { const invalidParams = { title: '', // Invalid: empty description: 'Valid description', projectId: '', // Invalid: empty priority: 'invalid' as TaskPriority, // Invalid priority estimatedHours: -5, // Invalid: negative tags: 'not-array', // Invalid: not array dependencies: 'not-array' // Invalid: not array }; const result = await epicService.createEpic(invalidParams as unknown); expect(result.success).toBe(false); expect(result.error).toContain('validation failed'); }); it('should validate update epic parameters', async () => { const invalidParams = { title: '', // Invalid: empty status: 'invalid' as TaskStatus, // Invalid status priority: 'invalid' as TaskPriority, // Invalid priority estimatedHours: -5, // Invalid: negative tags: 'not-array', // Invalid: not array dependencies: 'not-array' // Invalid: not array }; const result = await epicService.updateEpic('E001', invalidParams as unknown); expect(result.success).toBe(false); expect(result.error).toContain('validation failed'); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server