Skip to main content
Glama
epic-persistence-validation.test.ts24.2 kB
/** * Epic Persistence Validation Test * * Verifies that epic persistence works correctly without generating * scaffolding E001/E002/E003 assignments and instead creates * meaningful functional area-based epics. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { RDDEngine } from '../../core/rdd-engine.js'; import { AtomicTask, FunctionalArea } from '../../types/task.js'; import { ProjectContext } from '../../types/project-context.js'; import logger from '../../../../logger.js'; import { EpicContextResolver } from '../../services/epic-context-resolver.js'; // Mock all external dependencies to avoid live LLM calls vi.mock('../../../../utils/llmHelper.js', () => ({ performDirectLlmCall: vi.fn().mockResolvedValue(JSON.stringify({ isAtomic: true, confidence: 0.95, reasoning: 'Task is atomic and focused', estimatedHours: 0.1 })), performFormatAwareLlmCall: vi.fn().mockResolvedValue(JSON.stringify({ tasks: [{ title: 'Test Subtask', description: 'Test subtask description', estimatedHours: 0.1, acceptanceCriteria: ['Test criteria'], priority: 'medium' }] })), performFormatAwareLlmCallWithCentralizedConfig: vi.fn().mockResolvedValue(JSON.stringify({ tasks: [{ title: 'Test Subtask', description: 'Test subtask description', estimatedHours: 0.1, acceptanceCriteria: ['Test criteria'], priority: 'medium' }] })) })); vi.mock('../../utils/config-loader.js', () => ({ getVibeTaskManagerConfig: vi.fn().mockResolvedValue({ taskManager: { rddConfig: { maxDepth: 3, maxSubTasks: 50, minConfidence: 0.7, enableParallelDecomposition: false, epicTimeLimit: 400 }, openRouterConfig: { apiKey: 'test-api-key', baseUrl: 'https://test.openrouter.ai/api/v1', geminiModel: 'google/gemini-2.5-flash-preview', perplexityModel: 'perplexity/llama-3.1-sonar-small-128k-online', llm_mapping: { 'task_decomposition': 'google/gemini-2.5-flash-preview', 'atomic_detection': 'google/gemini-2.5-flash-preview', 'intent_recognition': 'google/gemini-2.5-flash-preview', 'default_generation': 'google/gemini-2.5-flash-preview' } } } }), getVibeTaskManagerOutputDir: vi.fn().mockReturnValue('/tmp/test-output'), getBaseOutputDir: vi.fn().mockReturnValue('/tmp'), getLLMModelForOperation: vi.fn().mockResolvedValue('test-model') })); vi.mock('fs-extra', async (importOriginal) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const actual = await importOriginal() as any; return { ...actual, ensureDir: vi.fn().mockResolvedValue(undefined), ensureDirSync: vi.fn().mockReturnValue(undefined), readFile: vi.fn().mockResolvedValue('{}'), writeFile: vi.fn().mockResolvedValue(undefined), pathExists: vi.fn().mockResolvedValue(true), stat: vi.fn().mockResolvedValue({ isFile: () => true, isDirectory: () => false }), remove: vi.fn().mockResolvedValue(undefined) }; }); // Mock auto-research detector vi.mock('../../services/auto-research-detector.js', () => ({ AutoResearchDetector: { getInstance: vi.fn().mockReturnValue({ evaluateResearchNeed: vi.fn().mockResolvedValue({ decision: { shouldTriggerResearch: false, confidence: 0.9, primaryReason: 'sufficient_context', reasoning: ['Test context is sufficient'], recommendedScope: { estimatedQueries: 0 } } }) }) } })); // Mock context enrichment service vi.mock('../../services/context-enrichment-service.js', () => ({ ContextEnrichmentService: { getInstance: vi.fn().mockReturnValue({ gatherContext: vi.fn().mockResolvedValue({ contextFiles: [], failedFiles: [], summary: { totalFiles: 0, totalSize: 0, averageRelevance: 0, topFileTypes: [], gatheringTime: 1 } }) }) } })); // Mock storage manager vi.mock('../../core/storage/storage-manager.js', () => ({ getStorageManager: vi.fn().mockResolvedValue({ projectExists: vi.fn().mockResolvedValue(true), epicExists: vi.fn().mockResolvedValue(false), taskExists: vi.fn().mockResolvedValue(false), dependencyExists: vi.fn().mockResolvedValue(false), getProject: vi.fn().mockResolvedValue({ success: true, data: { id: 'test-project', name: 'Test Project', epicIds: [] } }), getEpic: vi.fn().mockResolvedValue({ success: true, data: { id: 'test-epic', title: 'Test Epic', taskIds: [] } }), getTask: vi.fn().mockResolvedValue({ success: true, data: { id: 'test-task', title: 'Test Task' } }), updateProject: vi.fn().mockResolvedValue({ success: true }), updateEpic: vi.fn().mockResolvedValue({ success: true }), updateTask: vi.fn().mockResolvedValue({ success: true }), getDependenciesForTask: vi.fn().mockResolvedValue({ success: true, data: [] }) }) })); // Mock epic service vi.mock('../../services/epic-service.js', () => ({ getEpicService: vi.fn().mockReturnValue({ createEpic: vi.fn().mockResolvedValue({ success: true, data: { id: 'test-created-epic', title: 'Test Created Epic' } }) }) })); // Mock project operations vi.mock('../../core/operations/project-operations.js', () => ({ getProjectOperations: vi.fn().mockReturnValue({ getProject: vi.fn().mockResolvedValue({ success: true, data: { id: 'test-project', name: 'Test Project', epicIds: [] } }) }) })); describe('Epic Persistence Validation', () => { let rddEngine: RDDEngine; let projectContext: ProjectContext; let projectId: string; let epicResolver: EpicContextResolver; beforeEach(async () => { // Initialize RDD engine for testing epic assignment const configResult = await import('../../utils/config-loader.js'); const config = await configResult.getVibeTaskManagerConfig(); rddEngine = new RDDEngine(config.taskManager.openRouterConfig, config.taskManager.rddConfig); projectId = `test-project-${Date.now()}`; epicResolver = EpicContextResolver.getInstance(); // Create minimal project context projectContext = { projectId, projectPath: '/test/project', projectName: 'Test Project', description: 'Test project for epic validation', languages: ['TypeScript'], frameworks: ['React'], buildTools: ['Vite'], tools: ['ESLint'], configFiles: ['package.json'], entryPoints: ['src/main.ts'], architecturalPatterns: ['MVC'], existingTasks: [], codebaseSize: 'medium', teamSize: 3, complexity: 'medium', structure: { sourceDirectories: ['src'], testDirectories: ['__tests__'], docDirectories: ['docs'], buildDirectories: ['dist'] }, dependencies: { production: ['react'], development: ['typescript'], external: [] }, metadata: { createdAt: new Date(), updatedAt: new Date(), version: '1.0.0', source: 'manual' } }; }); describe('Dynamic Functional Area Epic Creation', () => { it('should create project-specific functional area epics using dynamic extraction', async () => { logger.info('🏛️ Testing dynamic functional area epic creation vs scaffolding'); // Test cases with different project domains to trigger dynamic extraction const testCases = [ { title: 'User authentication system', description: 'Implement secure user login and registration with OAuth support', expectedArea: 'auth' }, { title: 'REST API endpoints', description: 'Create RESTful API endpoints for data management and CRUD operations', expectedArea: 'api' }, { title: 'React components library', description: 'Build reusable UI components for the frontend interface', expectedArea: 'ui' }, { title: 'Database schema design', description: 'Design and implement database tables and relationships for data storage', expectedArea: 'data' } ]; const createdEpics: string[] = []; for (const testCase of testCases) { // Create task that should trigger dynamic functional area extraction const testTask: AtomicTask = { id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, title: testCase.title, description: testCase.description, status: 'pending', priority: 'medium', type: 'development', functionalArea: 'data-management', // This will be overridden by dynamic extraction estimatedHours: 8, actualHours: 0, epicId: 'temp-epic', projectId, dependencies: [], dependents: [], filePaths: [], acceptanceCriteria: ['Implementation completed and tested'], testingRequirements: { unitTests: [], integrationTests: [], performanceTests: [], coverageTarget: 80 }, performanceCriteria: {}, qualityCriteria: { codeQuality: [], documentation: [], typeScript: true, eslint: true }, integrationCriteria: { compatibility: [], patterns: [] }, validationMethods: { automated: [], manual: [] }, assignedAgent: undefined, executionContext: undefined, createdAt: new Date(), updatedAt: new Date(), startedAt: undefined, completedAt: undefined, createdBy: 'test-system', tags: [], metadata: { createdAt: new Date(), updatedAt: new Date(), createdBy: 'test-system', tags: [] } }; // Use RDD engine decomposition which should trigger dynamic functional area extraction const result = await rddEngine.decomposeTask(testTask, projectContext, 0); expect(result.success).toBe(true); expect(result.subTasks).toBeDefined(); if (result.subTasks && result.subTasks.length > 0) { const firstTask = result.subTasks[0]; // Verify epic ID is NOT scaffolding format const isScaffoldingEpic = /^E0{0,2}[123]$/.test(firstTask.epicId); expect(isScaffoldingEpic).toBe(false); // Verify epic ID is not default expect(firstTask.epicId).not.toBe('default-epic'); expect(firstTask.epicId).not.toBe('temp-epic'); // Epic should be project-specific or functional area specific const isMeaningfulEpic = firstTask.epicId.includes(projectId) || firstTask.epicId.includes(testCase.expectedArea) || firstTask.epicId.includes('main') || firstTask.epicId.includes('epic'); expect(isMeaningfulEpic).toBe(true); createdEpics.push(firstTask.epicId); logger.info(`✅ Dynamic extraction for "${testCase.title}": ${firstTask.epicId}`); } } // Verify epics were created (may reuse existing ones) expect(createdEpics.length).toBeGreaterThan(0); logger.info(`✅ Successfully validated ${createdEpics.length} dynamically-extracted epics`); }, 30000); it('should assign meaningful epic IDs through decomposition process', async () => { logger.info('💾 Testing epic assignment through decomposition'); const testCases = [ { title: 'Authentication system', description: 'User login and security features' }, { title: 'Content management', description: 'Create and manage content items' }, { title: 'UI component library', description: 'Reusable interface components' } ]; for (const testCase of testCases) { const testTask: AtomicTask = { id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, title: testCase.title, description: testCase.description, status: 'pending', priority: 'high', type: 'development', functionalArea: 'data-management', // Will be dynamically assigned estimatedHours: 6, actualHours: 0, epicId: 'temp-epic', projectId, dependencies: [], dependents: [], filePaths: [], acceptanceCriteria: ['Feature implemented and tested'], testingRequirements: { unitTests: [], integrationTests: [], performanceTests: [], coverageTarget: 80 }, performanceCriteria: {}, qualityCriteria: { codeQuality: [], documentation: [], typeScript: true, eslint: true }, integrationCriteria: { compatibility: [], patterns: [] }, validationMethods: { automated: [], manual: [] }, assignedAgent: undefined, executionContext: undefined, createdAt: new Date(), updatedAt: new Date(), startedAt: undefined, completedAt: undefined, createdBy: 'test-system', tags: [], metadata: { createdAt: new Date(), updatedAt: new Date(), createdBy: 'test-system', tags: [] } }; const result = await rddEngine.decomposeTask(testTask, projectContext, 0); expect(result.success).toBe(true); if (result.subTasks && result.subTasks.length > 0) { const task = result.subTasks[0]; // Verify epic is not using default or scaffolding pattern expect(task.epicId).not.toBe('default-epic'); expect(task.epicId).not.toBe('temp-epic'); expect(task.epicId).not.toMatch(/^E0{0,2}[123]$/); // Epic should be meaningful const isMeaningful = task.epicId.includes(projectId) || task.epicId.includes('main') || task.epicId.includes('epic') || /auth|api|ui|data/.test(task.epicId); expect(isMeaningful).toBe(true); logger.info(`✅ Epic assigned: ${task.epicId} for "${testCase.title}"`); } } logger.info('✅ All epics assigned correctly without scaffolding patterns'); }, 30000); it('should consistently assign epics for similar functional areas', async () => { logger.info('🔄 Testing consistent epic assignment for similar tasks'); // Create two tasks with similar functional areas const similarTasks = [ { title: 'User profile management', description: 'Manage user profiles and settings', expectedPattern: /auth|user|main|epic/i }, { title: 'User authentication flow', description: 'Handle user login and authentication process', expectedPattern: /auth|user|main|epic/i } ]; const assignedEpics: string[] = []; for (const taskInfo of similarTasks) { const testTask: AtomicTask = { id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, title: taskInfo.title, description: taskInfo.description, status: 'pending', priority: 'high', type: 'development', functionalArea: 'user-management', estimatedHours: 4, actualHours: 0, epicId: 'temp-epic', projectId, dependencies: [], dependents: [], filePaths: [], acceptanceCriteria: ['Feature completed'], testingRequirements: { unitTests: [], integrationTests: [], performanceTests: [], coverageTarget: 80 }, performanceCriteria: {}, qualityCriteria: { codeQuality: [], documentation: [], typeScript: true, eslint: true }, integrationCriteria: { compatibility: [], patterns: [] }, validationMethods: { automated: [], manual: [] }, assignedAgent: undefined, executionContext: undefined, createdAt: new Date(), updatedAt: new Date(), startedAt: undefined, completedAt: undefined, createdBy: 'test-system', tags: [], metadata: { createdAt: new Date(), updatedAt: new Date(), createdBy: 'test-system', tags: [] } }; const result = await rddEngine.decomposeTask(testTask, projectContext, 0); expect(result.success).toBe(true); if (result.subTasks && result.subTasks.length > 0) { const task = result.subTasks[0]; // Verify not scaffolding expect(task.epicId).not.toMatch(/^E0{0,2}[123]$/); expect(task.epicId).not.toBe('default-epic'); // Should match expected pattern expect(taskInfo.expectedPattern.test(task.epicId)).toBe(true); assignedEpics.push(task.epicId); logger.info(`✅ Epic for "${taskInfo.title}": ${task.epicId}`); } } expect(assignedEpics.length).toBe(2); logger.info(`✅ Consistent epic assignment validated`); }, 30000); }); describe('Anti-Scaffolding Validation', () => { it('should never generate E001, E002, E003 epic IDs', async () => { logger.info('🚫 Testing prevention of scaffolding epic IDs'); const testScenarios = [ { context: 'Simple task', functionalArea: 'data-management' }, { context: 'Complex task', functionalArea: 'integration' }, { context: 'Basic task', functionalArea: 'admin' }, { context: 'Advanced task', functionalArea: 'performance' } ] as const; const generatedEpicIds: string[] = []; for (const scenario of testScenarios) { const result = await epicResolver.resolveEpicContext({ projectId, taskContext: { title: `${scenario.context} for ${scenario.functionalArea}`, description: `Implement ${scenario.context} in ${scenario.functionalArea} area`, functionalArea: scenario.functionalArea, type: 'development', priority: 'medium' } }); expect(result).toBeDefined(); generatedEpicIds.push(result.epicId); // Verify NOT scaffolding patterns expect(result.epicId).not.toBe('E001'); expect(result.epicId).not.toBe('E002'); expect(result.epicId).not.toBe('E003'); expect(result.epicId).not.toBe('E1'); expect(result.epicId).not.toBe('E2'); expect(result.epicId).not.toBe('E3'); expect(result.epicId).not.toMatch(/^E0{0,2}[123]$/); // Verify NOT generic patterns expect(result.epicId).not.toBe('default-epic'); expect(result.epicId).not.toMatch(/scaffolding|setup|basic|generic/i); logger.info(`✅ Non-scaffolding epic: ${result.epicId} for ${scenario.functionalArea}`); } // Verify all epic IDs are meaningful and unique where appropriate const meaningfulPattern = /auth|user|content|data|integration|admin|ui|performance|management/i; const meaningfulEpics = generatedEpicIds.filter(id => meaningfulPattern.test(id)); expect(meaningfulEpics.length).toBeGreaterThan(0); logger.info(`✅ Generated ${meaningfulEpics.length} meaningful epic IDs out of ${generatedEpicIds.length} total`); }, 30000); it('should generate domain-specific epic names and descriptions', async () => { logger.info('🎯 Testing domain-specific epic content generation'); const domainCases = [ { functionalArea: 'authentication' as FunctionalArea, expectedTerms: ['auth', 'login', 'security', 'user', 'access'] }, { functionalArea: 'content-management' as FunctionalArea, expectedTerms: ['content', 'manage', 'create', 'edit', 'publish'] }, { functionalArea: 'user-management' as FunctionalArea, expectedTerms: ['user', 'profile', 'manage', 'account', 'settings'] } ]; for (const domainCase of domainCases) { const result = await epicResolver.resolveEpicContext({ projectId, taskContext: { title: `Implement ${domainCase.functionalArea} features`, description: `Complete ${domainCase.functionalArea} implementation`, functionalArea: domainCase.functionalArea, type: 'development', priority: 'high' } }); expect(result).toBeDefined(); // Check epic content for domain-specific terms const epicContent = `${result.epicId} ${result.epicName || ''}`.toLowerCase(); const hasRelevantTerms = domainCase.expectedTerms.some(term => epicContent.includes(term) ); expect(hasRelevantTerms).toBe(true); logger.info(`✅ Domain-specific epic for ${domainCase.functionalArea}: ${result.epicId}`); } logger.info('✅ All epics contain domain-specific terminology'); }, 30000); }); describe('Epic Persistence Edge Cases', () => { it('should handle missing project context gracefully', async () => { logger.info('⚠️ Testing epic creation with minimal context'); const result = await epicResolver.resolveEpicContext({ projectId: 'minimal-project', taskContext: { title: 'Basic task', description: 'Simple task description', functionalArea: 'data-management', type: 'development', priority: 'low' } }); expect(result).toBeDefined(); expect(result.epicId).toBeDefined(); expect(result.created).toBe(true); // Even with minimal context, should not fall back to scaffolding expect(result.epicId).not.toMatch(/^E0{0,2}[123]$/); expect(result.epicId).not.toBe('default-epic'); logger.info(`✅ Graceful handling with minimal context: ${result.epicId}`); }, 30000); it('should create fallback epic when functional area epic creation fails', async () => { logger.info('🔄 Testing fallback epic creation'); // Create a scenario that might cause epic creation to use fallback const result = await epicResolver.resolveEpicContext({ projectId: 'fallback-test-project', taskContext: { title: 'Edge case task', description: 'Task that might trigger fallback logic', functionalArea: 'integration', type: 'development', priority: 'medium' } }); expect(result).toBeDefined(); expect(result.epicId).toBeDefined(); // Even fallback should be meaningful, not scaffolding expect(result.epicId).not.toMatch(/^E0{0,2}[123]$/); expect(result.epicId).not.toBe('default-epic'); // Fallback epic should be project-specific const isProjectSpecific = result.epicId.includes('fallback-test-project') || result.epicId.includes('main') || result.epicId.includes('integration'); expect(isProjectSpecific).toBe(true); logger.info(`✅ Fallback epic creation: ${result.epicId}`); }, 30000); }); });

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