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
JavaScript
/**
* 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);
}