UNPKG

task-master-neo-sdlc

Version:

Enhanced task management system with Neo SDLC agents and MCP tools for comprehensive, AI-driven software development lifecycle management.

713 lines (605 loc) 21.7 kB
/** * TDD Framework for Neo System * * Implements Test-Driven Development workflow with maker-checker pattern. * Coordinates between development agents and QA agents. */ import { log } from '../../utils/logging.js'; import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; // Import test framework utilities import { detectTestFrameworks, recommendTestFrameworks } from './testing/test-frameworks.js'; // Import CI/CD integration utilities import { detectCICDSystem } from './testing/ci-cd-integration.js'; // Import test metrics utilities import { collectTestMetrics, analyzeTestMetrics } from './testing/test-metrics.js'; export class TDDFramework { constructor(agentWorkflowSystem) { this.agentWorkflowSystem = agentWorkflowSystem; this.workflows = new Map(); log.info('TDD Framework initialized'); } /** * Create a TDD workflow * @param {Object} options - Workflow options * @param {string} options.id - Workflow ID * @param {string} options.taskMasterId - Task Master task ID * @param {string} options.makerAgentId - Maker agent ID (developer) * @param {string} options.checkerAgentId - Checker agent ID (QA) * @param {Object} options.testConfig - Test configuration * @returns {Promise<Object>} Created workflow */ async createTDDWorkflow(options) { const { id, taskMasterId, makerAgentId, checkerAgentId, testConfig } = options; if (!id || !taskMasterId || !makerAgentId || !checkerAgentId) { throw new Error('TDD workflow must have id, taskMasterId, makerAgentId, and checkerAgentId'); } log.info(`Creating TDD workflow ${id} for task ${taskMasterId}`); // Fetch the task from Task Master const task = await this.agentWorkflowSystem.fetchTaskMasterTaskById(taskMasterId); if (!task) { throw new Error(`Task with ID ${taskMasterId} not found`); } // Create the TDD workflow const workflow = { id, taskMasterId, makerAgentId, checkerAgentId, testConfig: testConfig || { framework: 'jest', coverage: 80, testTypes: ['unit', 'integration'] }, status: 'created', steps: [ { id: `${id}-step-1`, name: 'Write Tests', status: 'pending', agentId: checkerAgentId, role: 'checker', action: 'createTest', dependencies: [] }, { id: `${id}-step-2`, name: 'Implement Feature', status: 'pending', agentId: makerAgentId, role: 'maker', action: 'implementFeature', dependencies: [`${id}-step-1`] }, { id: `${id}-step-3`, name: 'Validate Implementation', status: 'pending', agentId: checkerAgentId, role: 'checker', action: 'validateImplementation', dependencies: [`${id}-step-2`] }, { id: `${id}-step-4`, name: 'Refactor Implementation', status: 'pending', agentId: makerAgentId, role: 'maker', action: 'refactorImplementation', dependencies: [`${id}-step-3`] }, { id: `${id}-step-5`, name: 'Final Validation', status: 'pending', agentId: checkerAgentId, role: 'checker', action: 'validateImplementation', dependencies: [`${id}-step-4`] } ], results: {}, createdAt: new Date(), updatedAt: new Date() }; // Store the workflow this.workflows.set(id, workflow); return workflow; } /** * Execute a TDD workflow * @param {string} workflowId - Workflow ID * @returns {Promise<Object>} Execution result */ async executeTDDWorkflow(workflowId) { const workflow = this.workflows.get(workflowId); if (!workflow) { throw new Error(`TDD workflow with ID ${workflowId} not found`); } log.info(`Executing TDD workflow ${workflowId}`); try { // Update workflow status workflow.status = 'running'; workflow.updatedAt = new Date(); // Execute each step in sequence for (const step of workflow.steps) { // Check if dependencies are satisfied const canExecute = this.checkDependencies(step, workflow); if (!canExecute) { log.warn(`Cannot execute step ${step.id} because dependencies are not satisfied`); continue; } log.info(`Executing step ${step.id}: ${step.name}`); // Update step status step.status = 'running'; workflow.updatedAt = new Date(); // Execute the step const result = await this.executeStep(step, workflow); // Store the result workflow.results[step.id] = result; // Update step status step.status = result.success ? 'completed' : 'failed'; workflow.updatedAt = new Date(); // If step failed, mark remaining steps as skipped if (!result.success) { log.warn(`Step ${step.id} failed, skipping remaining steps`); for (const remainingStep of workflow.steps) { if (remainingStep.status === 'pending') { remainingStep.status = 'skipped'; } } workflow.status = 'failed'; workflow.updatedAt = new Date(); return { success: false, workflowId, error: result.error, results: workflow.results }; } } // Check if all steps are completed const allCompleted = workflow.steps.every(step => step.status === 'completed'); // Update workflow status workflow.status = allCompleted ? 'completed' : 'failed'; workflow.updatedAt = new Date(); return { success: allCompleted, workflowId, results: workflow.results }; } catch (error) { log.error(`Error executing TDD workflow ${workflowId}: ${error.message}`); // Update workflow status workflow.status = 'failed'; workflow.updatedAt = new Date(); return { success: false, workflowId, error: error.message, results: workflow.results }; } } /** * Check if step dependencies are satisfied * @param {Object} step - Workflow step * @param {Object} workflow - Workflow * @returns {boolean} Whether dependencies are satisfied */ checkDependencies(step, workflow) { if (!step.dependencies || step.dependencies.length === 0) { return true; } return step.dependencies.every(depId => { const depStep = workflow.steps.find(s => s.id === depId); return depStep && depStep.status === 'completed'; }); } /** * Execute a workflow step * @param {Object} step - Workflow step * @param {Object} workflow - Workflow * @returns {Promise<Object>} Execution result */ async executeStep(step, workflow) { try { // Get the agent const agent = this.agentWorkflowSystem.agents.get(step.agentId); if (!agent) { throw new Error(`Agent with ID ${step.agentId} not found`); } // Get the task const task = await this.agentWorkflowSystem.fetchTaskMasterTaskById(workflow.taskMasterId); if (!task) { throw new Error(`Task with ID ${workflow.taskMasterId} not found`); } // Execute the step based on action let result; switch (step.action) { case 'createTest': result = await this.executeCreateTest(agent, task, workflow); break; case 'implementFeature': result = await this.executeImplementFeature(agent, task, workflow); break; case 'validateImplementation': result = await this.executeValidateImplementation(agent, task, workflow); break; case 'refactorImplementation': result = await this.executeRefactorImplementation(agent, task, workflow); break; default: throw new Error(`Unknown action: ${step.action}`); } return { success: true, stepId: step.id, action: step.action, result }; } catch (error) { log.error(`Error executing step ${step.id}: ${error.message}`); return { success: false, stepId: step.id, action: step.action, error: error.message }; } } /** * Execute createTest action * @param {Object} agent - QA agent * @param {Object} task - Task * @param {Object} workflow - Workflow * @returns {Promise<Object>} Execution result */ async executeCreateTest(agent, task, workflow) { log.info(`Executing createTest for task ${task.id} with agent ${agent.id}`); // Extract acceptance criteria from task const acceptanceCriteria = task.acceptanceCriteria || []; // Determine test target const testTarget = this.determineTestTarget(task); // Determine project root const projectRoot = this.agentWorkflowSystem.projectRoot; // Detect project type const projectType = this.determineProjectType(projectRoot); // Detect existing test frameworks const existingFrameworks = detectTestFrameworks(projectRoot); // Recommend test frameworks based on project type const recommendedFrameworks = recommendTestFrameworks({ type: projectType, features: task.features || [], projectRoot }); // Determine test framework to use const testFramework = workflow.testConfig.framework || recommendedFrameworks[workflow.testConfig.testTypes[0]] || existingFrameworks[0] || 'jest'; // Create test parameters const params = { testTarget, testTypes: workflow.testConfig.testTypes, coverage: workflow.testConfig.coverage, acceptanceCriteria, projectType }; // Create context const context = { taskId: task.id, workflowId: workflow.id, testFramework, projectRoot, projectType, features: task.features || [], runTests: false, // Don't run tests yet since implementation doesn't exist useAI: workflow.testConfig.useAI !== undefined ? workflow.testConfig.useAI : true, setupCICD: workflow.testConfig.setupCICD !== undefined ? workflow.testConfig.setupCICD : false, collectMetrics: workflow.testConfig.collectMetrics !== undefined ? workflow.testConfig.collectMetrics : false }; // Call the agent's createTest method const result = await agent.createTest(params, context); // Store test framework in workflow for future steps workflow.testConfig.framework = result.testFramework || testFramework; return result; } /** * Execute implementFeature action * @param {Object} agent - Developer agent * @param {Object} task - Task * @param {Object} workflow - Workflow * @returns {Promise<Object>} Execution result */ async executeImplementFeature(agent, task, workflow) { log.info(`Executing implementFeature for task ${task.id} with agent ${agent.id}`); // Get the test result from the previous step const createTestStep = workflow.steps.find(s => s.action === 'createTest'); const createTestResult = workflow.results[createTestStep.id]?.result; if (!createTestResult) { throw new Error('No test result found from createTest step'); } // Determine implementation target const implementationTarget = this.determineImplementationTarget(task); // Create implementation parameters const params = { implementationTarget, testPath: createTestResult.testPath, testCode: createTestResult.testCode, acceptanceCriteria: task.acceptanceCriteria || [] }; // Create context const context = { taskId: task.id, workflowId: workflow.id, tdd: true // Indicate that this is a TDD workflow }; // Call the agent's implementFeature method const result = await agent.implementFeature(params, context); return result; } /** * Execute validateImplementation action * @param {Object} agent - QA agent * @param {Object} task - Task * @param {Object} workflow - Workflow * @returns {Promise<Object>} Execution result */ async executeValidateImplementation(agent, task, workflow) { log.info(`Executing validateImplementation for task ${task.id} with agent ${agent.id}`); // Get the test result from the createTest step const createTestStep = workflow.steps.find(s => s.action === 'createTest'); const createTestResult = workflow.results[createTestStep.id]?.result; if (!createTestResult) { throw new Error('No test result found from createTest step'); } // Get the implementation result from the previous step const implementStep = workflow.steps.find(s => s.action === 'implementFeature' || s.action === 'refactorImplementation' ); const implementResult = workflow.results[implementStep.id]?.result; if (!implementResult) { throw new Error('No implementation result found'); } // Determine project root const projectRoot = this.agentWorkflowSystem.projectRoot; // Create validation parameters const params = { implementationPath: implementResult.implementationPath, testPath: createTestResult.testPath, coverage: workflow.testConfig.coverage, testFramework: workflow.testConfig.framework, testTypes: workflow.testConfig.testTypes }; // Create context const context = { taskId: task.id, workflowId: workflow.id, testFramework: workflow.testConfig.framework, projectRoot, runTests: true, collectMetrics: workflow.testConfig.collectMetrics !== undefined ? workflow.testConfig.collectMetrics : true }; // Call the agent's validateImplementation method const result = await agent.validateImplementation(params, context); // If validation failed and metrics were collected, store them in the workflow if (!result.passed && result.metrics) { workflow.metrics = result.metrics; } return result; } /** * Execute refactorImplementation action * @param {Object} agent - Developer agent * @param {Object} task - Task * @param {Object} workflow - Workflow * @returns {Promise<Object>} Execution result */ async executeRefactorImplementation(agent, task, workflow) { log.info(`Executing refactorImplementation for task ${task.id} with agent ${agent.id}`); // Get the validation result from the previous step const validateStep = workflow.steps.find(s => s.action === 'validateImplementation'); const validateResult = workflow.results[validateStep.id]?.result; if (!validateResult) { throw new Error('No validation result found'); } // If validation passed, no need to refactor if (validateResult.passed) { log.info('Validation passed, no refactoring needed'); return { implementationPath: validateResult.implementationPath, testPath: validateResult.testPath, refactored: false, message: 'No refactoring needed, implementation passed validation' }; } // Get the implementation result from the implementFeature step const implementStep = workflow.steps.find(s => s.action === 'implementFeature'); const implementResult = workflow.results[implementStep.id]?.result; if (!implementResult) { throw new Error('No implementation result found'); } // Get the test result from the createTest step const createTestStep = workflow.steps.find(s => s.action === 'createTest'); const createTestResult = workflow.results[createTestStep.id]?.result; if (!createTestResult) { throw new Error('No test result found'); } // Determine project root const projectRoot = this.agentWorkflowSystem.projectRoot; // Create refactor parameters const params = { implementationPath: implementResult.implementationPath, testPath: validateResult.testPath, feedback: validateResult.feedback, testResults: validateResult.testResults, testCode: createTestResult.testCode, testFramework: workflow.testConfig.framework, metrics: workflow.metrics || validateResult.metrics }; // Create context const context = { taskId: task.id, workflowId: workflow.id, projectRoot, tdd: true, // Indicate that this is a TDD workflow useAI: workflow.testConfig.useAI !== undefined ? workflow.testConfig.useAI : true }; // If the agent is a developer agent, call its refactorImplementation method if (agent.role === 'developer') { const result = await agent.refactorImplementation(params, context); return result; } // If the agent is not a developer agent, use the QA agent to generate refactoring suggestions // Get the QA agent const qaAgent = this.agentWorkflowSystem.agents.get(workflow.checkerAgentId); if (!qaAgent) { throw new Error(`QA agent with ID ${workflow.checkerAgentId} not found`); } // Generate refactoring suggestions const refactoringSuggestions = await qaAgent.generateRefactoringSuggestions(params, context); // Apply refactoring suggestions if (refactoringSuggestions.refactoredCode) { // Write refactored code to file fs.writeFileSync(implementResult.implementationPath, refactoringSuggestions.refactoredCode, 'utf8'); return { implementationPath: implementResult.implementationPath, refactoredCode: refactoringSuggestions.refactoredCode, explanation: refactoringSuggestions.explanation, refactored: true, message: 'Implementation refactored based on test results' }; } else { return { implementationPath: implementResult.implementationPath, refactored: false, message: 'Could not generate refactoring suggestions' }; } } /** * Determine test target from task * @param {Object} task - Task * @returns {string} Test target */ determineTestTarget(task) { // Extract from task title or description const title = task.title || ''; const description = task.description || ''; // Look for component or module names const componentMatch = title.match(/(?:Implement|Create|Build)\s+(\w+)(?:\s+Component)?/i) || description.match(/(?:Implement|Create|Build)\s+(\w+)(?:\s+Component)?/i); if (componentMatch && componentMatch[1]) { return componentMatch[1]; } // Default to a sanitized version of the task title return title .replace(/[^a-zA-Z0-9]/g, '') .replace(/(?:Implement|Create|Build)/i, ''); } /** * Determine project type based on files and dependencies * @param {string} projectRoot - Project root directory * @returns {string} Project type (react, vue, angular, node, etc.) */ determineProjectType(projectRoot) { try { // Check package.json const packageJsonPath = path.join(projectRoot, 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for React if (dependencies.react) { return 'react'; } // Check for Vue if (dependencies.vue) { return 'vue'; } // Check for Angular if (dependencies['@angular/core']) { return 'angular'; } // Check for Next.js if (dependencies.next) { return 'nextjs'; } // Check for Express if (dependencies.express) { return 'express'; } // Check for NestJS if (dependencies['@nestjs/core']) { return 'nestjs'; } } // Check for specific files if (fs.existsSync(path.join(projectRoot, 'angular.json'))) { return 'angular'; } if (fs.existsSync(path.join(projectRoot, 'vue.config.js'))) { return 'vue'; } if (fs.existsSync(path.join(projectRoot, 'next.config.js'))) { return 'nextjs'; } // Default to node return 'node'; } catch (error) { log.warn(`Error determining project type: ${error.message}`); return 'node'; } } /** * Determine implementation target from task * @param {Object} task - Task * @returns {string} Implementation target */ determineImplementationTarget(task) { // Use the same logic as determineTestTarget for now return this.determineTestTarget(task); } /** * Get TDD workflow by ID * @param {string} workflowId - Workflow ID * @returns {Object} Workflow */ getTDDWorkflow(workflowId) { return this.workflows.get(workflowId); } /** * Get all TDD workflows * @returns {Array<Object>} Workflows */ getAllTDDWorkflows() { return Array.from(this.workflows.values()); } /** * Get TDD workflows by task ID * @param {string} taskId - Task ID * @returns {Array<Object>} Workflows */ getTDDWorkflowsByTaskId(taskId) { return Array.from(this.workflows.values()) .filter(workflow => workflow.taskMasterId === taskId); } } // Export a factory function to create TDD frameworks export function createTDDFramework(agentWorkflowSystem) { return new TDDFramework(agentWorkflowSystem); }