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.

560 lines (489 loc) 21.2 kB
/** * AI-powered Test Generator * * Uses AI to generate sophisticated test cases and code based on * component/module analysis and acceptance criteria. */ import { log } from '../../../utils/logging.js'; import fs from 'fs'; import path from 'path'; import { getFrameworkConfig, getTestTypeConfig } from './test-frameworks.js'; import { getAvailableAIModel } from '../../../../scripts/modules/ai-services.js'; import { loadModule } from '../../../utils/module-loader.js'; /** * Generate test cases using AI * @param {Object} params - Test generation parameters * @param {string} params.testTarget - Target component or module to test * @param {Array<string>} params.testTypes - Types of tests to create * @param {Array<Object>} params.acceptanceCriteria - Acceptance criteria * @param {string} params.implementationPath - Path to implementation (if available) * @param {string} params.framework - Test framework to use * @param {Object} options - Additional options * @returns {Promise<Array<Object>>} Generated test cases */ export async function generateTestCasesWithAI(params, options = {}) { const { testTarget, testTypes, acceptanceCriteria = [], implementationPath, framework = 'jest' } = params; log.info(`Generating AI test cases for ${testTarget} using ${framework}`); try { // Get implementation code if available let implementationCode = ''; if (implementationPath && fs.existsSync(implementationPath)) { implementationCode = fs.readFileSync(implementationPath, 'utf8'); } // Prepare prompt for AI const prompt = buildTestCasePrompt(testTarget, testTypes, acceptanceCriteria, implementationCode, framework); // Get AI model const { type: modelType, client } = getAvailableAIModel(); // Call AI model let response; if (modelType === 'claude') { response = await client.messages.create({ model: options.model || 'claude-3-opus-20240229', max_tokens: options.maxTokens || 4000, temperature: options.temperature || 0.2, system: "You are an expert software testing specialist. Your task is to generate comprehensive test cases for software components based on acceptance criteria and implementation details. Focus on edge cases, error handling, and complete test coverage.", messages: [ { role: 'user', content: prompt } ] }); return parseTestCasesFromAIResponse(response.content[0].text, testTypes); } else if (modelType === 'perplexity') { response = await client.chat.completions.create({ model: options.model || 'sonar-medium-online', messages: [ { role: 'system', content: "You are an expert software testing specialist. Your task is to generate comprehensive test cases for software components based on acceptance criteria and implementation details. Focus on edge cases, error handling, and complete test coverage." }, { role: 'user', content: prompt } ], temperature: options.temperature || 0.2 }); return parseTestCasesFromAIResponse(response.choices[0].message.content, testTypes); } else { throw new Error('No AI model available'); } } catch (error) { log.error(`Error generating AI test cases: ${error.message}`); // Fallback to basic test cases return generateBasicTestCases(testTarget, testTypes, acceptanceCriteria); } } /** * Generate test code using AI * @param {Object} params - Test generation parameters * @param {string} params.testTarget - Target component or module to test * @param {Array<Object>} params.testCases - Test cases to implement * @param {string} params.framework - Test framework to use * @param {Array<string>} params.testTypes - Types of tests to create * @param {string} params.implementationPath - Path to implementation (if available) * @param {Object} options - Additional options * @returns {Promise<string>} Generated test code */ export async function generateTestCodeWithAI(params, options = {}) { const { testTarget, testCases, framework = 'jest', testTypes, implementationPath } = params; log.info(`Generating AI test code for ${testTarget} using ${framework}`); try { // Get implementation code if available let implementationCode = ''; if (implementationPath && fs.existsSync(implementationPath)) { implementationCode = fs.readFileSync(implementationPath, 'utf8'); } // Get framework configuration const frameworkConfig = getFrameworkConfig(framework); // Prepare prompt for AI const prompt = buildTestCodePrompt(testTarget, testCases, framework, testTypes, implementationCode, frameworkConfig); // Get AI model const { type: modelType, client } = getAvailableAIModel(); // Call AI model let response; if (modelType === 'claude') { response = await client.messages.create({ model: options.model || 'claude-3-opus-20240229', max_tokens: options.maxTokens || 8000, temperature: options.temperature || 0.2, system: "You are an expert software testing engineer. Your task is to generate high-quality test code for software components based on test cases and implementation details. Follow best practices for the specified test framework and ensure the tests are comprehensive and maintainable.", messages: [ { role: 'user', content: prompt } ] }); return extractTestCodeFromAIResponse(response.content[0].text); } else if (modelType === 'perplexity') { response = await client.chat.completions.create({ model: options.model || 'sonar-medium-online', messages: [ { role: 'system', content: "You are an expert software testing engineer. Your task is to generate high-quality test code for software components based on test cases and implementation details. Follow best practices for the specified test framework and ensure the tests are comprehensive and maintainable." }, { role: 'user', content: prompt } ], temperature: options.temperature || 0.2 }); return extractTestCodeFromAIResponse(response.choices[0].message.content); } else { throw new Error('No AI model available'); } } catch (error) { log.error(`Error generating AI test code: ${error.message}`); // Fallback to template-based test code return generateTemplateTestCode(testTarget, testCases, framework, testTypes); } } /** * Build prompt for test case generation * @param {string} testTarget - Target component or module to test * @param {Array<string>} testTypes - Types of tests to create * @param {Array<Object>} acceptanceCriteria - Acceptance criteria * @param {string} implementationCode - Implementation code (if available) * @param {string} framework - Test framework to use * @returns {string} Prompt for AI */ function buildTestCasePrompt(testTarget, testTypes, acceptanceCriteria, implementationCode, framework) { let prompt = `Generate comprehensive test cases for the ${testTarget} component/module using the ${framework} framework.\n\n`; // Add test types prompt += `Test types to create: ${testTypes.join(', ')}\n\n`; // Add acceptance criteria if (acceptanceCriteria && acceptanceCriteria.length > 0) { prompt += 'Acceptance Criteria:\n'; acceptanceCriteria.forEach((criterion, index) => { if (typeof criterion === 'string') { prompt += `${index + 1}. ${criterion}\n`; } else if (criterion.criterion) { prompt += `${index + 1}. ${criterion.criterion}\n`; } }); prompt += '\n'; } // Add implementation code if available if (implementationCode) { prompt += 'Implementation Code:\n```\n'; prompt += implementationCode; prompt += '\n```\n\n'; } // Add instructions for output format prompt += `Please generate test cases in the following JSON format: [ { "type": "unit", "description": "Test case description", "scenario": "Scenario being tested", "expectedResult": "Expected result of the test", "mockDependencies": ["List of dependencies to mock"], "edgeCases": ["List of edge cases to consider"] }, ...more test cases... ] Include test cases for: 1. Happy path scenarios 2. Edge cases 3. Error handling 4. Boundary conditions 5. Performance considerations (if applicable) For each test type (${testTypes.join(', ')}), provide at least 3-5 test cases.`; return prompt; } /** * Build prompt for test code generation * @param {string} testTarget - Target component or module to test * @param {Array<Object>} testCases - Test cases to implement * @param {string} framework - Test framework to use * @param {Array<string>} testTypes - Types of tests to create * @param {string} implementationCode - Implementation code (if available) * @param {Object} frameworkConfig - Framework configuration * @returns {string} Prompt for AI */ function buildTestCodePrompt(testTarget, testCases, framework, testTypes, implementationCode, frameworkConfig) { let prompt = `Generate test code for the ${testTarget} component/module using the ${framework} framework.\n\n`; // Add test types prompt += `Test types to create: ${testTypes.join(', ')}\n\n`; // Add test cases prompt += 'Test Cases to Implement:\n'; testCases.forEach((testCase, index) => { prompt += `${index + 1}. ${testCase.description}\n`; if (testCase.scenario) prompt += ` Scenario: ${testCase.scenario}\n`; if (testCase.expectedResult) prompt += ` Expected Result: ${testCase.expectedResult}\n`; if (testCase.mockDependencies && testCase.mockDependencies.length > 0) { prompt += ` Mock Dependencies: ${testCase.mockDependencies.join(', ')}\n`; } if (testCase.edgeCases && testCase.edgeCases.length > 0) { prompt += ` Edge Cases: ${testCase.edgeCases.join(', ')}\n`; } prompt += '\n'; }); // Add implementation code if available if (implementationCode) { prompt += 'Implementation Code:\n```\n'; prompt += implementationCode; prompt += '\n```\n\n'; } // Add framework-specific instructions prompt += `Framework: ${framework}\n`; if (frameworkConfig) { if (frameworkConfig.type === 'unit') { prompt += `This is a ${frameworkConfig.type} testing framework. `; prompt += `Use appropriate assertions and mocking techniques for ${framework}.\n\n`; } else if (frameworkConfig.type === 'e2e') { prompt += `This is an end-to-end testing framework. `; prompt += `Use appropriate browser automation and assertions for ${framework}.\n\n`; } } // Add instructions for output format prompt += `Please generate complete test code that: 1. Includes all necessary imports 2. Sets up test fixtures and mocks 3. Implements all the test cases 4. Uses best practices for the ${framework} framework 5. Includes proper error handling and assertions 6. Is well-commented and maintainable Return ONLY the test code without any explanations or markdown formatting.`; return prompt; } /** * Parse test cases from AI response * @param {string} response - AI response * @param {Array<string>} testTypes - Types of tests to create * @returns {Array<Object>} Parsed test cases */ function parseTestCasesFromAIResponse(response, testTypes) { try { // Extract JSON from response const jsonMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/); if (jsonMatch) { const jsonStr = jsonMatch[0]; const testCases = JSON.parse(jsonStr); return testCases; } // If no JSON found, try to parse structured text const testCases = []; const testCaseBlocks = response.split(/Test Case \d+:|## Test Case \d+:/); for (let i = 1; i < testCaseBlocks.length; i++) { const block = testCaseBlocks[i].trim(); // Extract test case properties const typeMatch = block.match(/Type:?\s*(\w+)/i); const descriptionMatch = block.match(/Description:?\s*([^\n]+)/i); const scenarioMatch = block.match(/Scenario:?\s*([^\n]+)/i); const expectedMatch = block.match(/Expected Result:?\s*([^\n]+)/i); if (descriptionMatch) { const testCase = { type: typeMatch ? typeMatch[1].toLowerCase() : testTypes[0], description: descriptionMatch[1].trim(), scenario: scenarioMatch ? scenarioMatch[1].trim() : '', expectedResult: expectedMatch ? expectedMatch[1].trim() : '' }; // Extract mock dependencies const mockMatch = block.match(/Mock Dependencies:?\s*([^\n]+)/i); if (mockMatch) { testCase.mockDependencies = mockMatch[1].split(',').map(d => d.trim()); } // Extract edge cases const edgeMatch = block.match(/Edge Cases:?\s*([^\n]+)/i); if (edgeMatch) { testCase.edgeCases = edgeMatch[1].split(',').map(e => e.trim()); } testCases.push(testCase); } } return testCases.length > 0 ? testCases : generateBasicTestCases(testTarget, testTypes, []); } catch (error) { log.error(`Error parsing AI test cases: ${error.message}`); return generateBasicTestCases(testTarget, testTypes, []); } } /** * Extract test code from AI response * @param {string} response - AI response * @returns {string} Extracted test code */ function extractTestCodeFromAIResponse(response) { try { // Extract code block from response const codeMatch = response.match(/```(?:javascript|jsx|typescript|tsx)?\n([\s\S]*?)```/); if (codeMatch) { return codeMatch[1].trim(); } // If no code block found, return the whole response return response.trim(); } catch (error) { log.error(`Error extracting test code: ${error.message}`); return response; } } /** * Generate basic test cases * @param {string} testTarget - Target component or module to test * @param {Array<string>} testTypes - Types of tests to create * @param {Array<Object>} acceptanceCriteria - Acceptance criteria * @returns {Array<Object>} Generated test cases */ function generateBasicTestCases(testTarget, testTypes, acceptanceCriteria) { const testCases = []; // Generate basic test cases for each test type testTypes.forEach(type => { switch (type) { case 'unit': testCases.push( { type: 'unit', description: `Should render ${testTarget} without crashing`, scenario: 'Component is rendered with default props', expectedResult: 'Component renders without errors' }, { type: 'unit', description: `Should match snapshot`, scenario: 'Component is rendered with default props', expectedResult: 'Component matches the stored snapshot' } ); break; case 'integration': testCases.push( { type: 'integration', description: `Should interact correctly with dependencies`, scenario: 'Component interacts with its dependencies', expectedResult: 'Component behaves correctly with real dependencies' } ); break; case 'e2e': testCases.push( { type: 'e2e', description: `Should work in an end-to-end flow`, scenario: 'User interacts with the component in a real browser', expectedResult: 'Component behaves correctly in a real environment' } ); break; case 'api': testCases.push( { type: 'api', description: `Should return correct response for valid request`, scenario: 'API endpoint receives a valid request', expectedResult: 'API returns the expected response' }, { type: 'api', description: `Should handle error cases properly`, scenario: 'API endpoint receives an invalid request', expectedResult: 'API returns appropriate error response' } ); break; } }); // Add test cases from acceptance criteria if (acceptanceCriteria && acceptanceCriteria.length > 0) { acceptanceCriteria.forEach(criterion => { if (typeof criterion === 'string') { testCases.push({ type: testTypes.includes('unit') ? 'unit' : 'integration', description: `Should satisfy: ${criterion}`, scenario: `Testing acceptance criterion: ${criterion}`, expectedResult: 'Component behavior matches acceptance criterion' }); } else if (criterion.criterion) { testCases.push({ type: testTypes.includes('unit') ? 'unit' : 'integration', description: `Should satisfy: ${criterion.criterion}`, scenario: `Testing acceptance criterion: ${criterion.criterion}`, expectedResult: 'Component behavior matches acceptance criterion' }); } }); } return testCases; } /** * Generate template-based test code * @param {string} testTarget - Target component or module to test * @param {Array<Object>} testCases - Test cases to implement * @param {string} framework - Test framework to use * @param {Array<string>} testTypes - Types of tests to create * @returns {string} Generated test code */ function generateTemplateTestCode(testTarget, testCases, framework, testTypes) { // Determine if this is likely a React component const isReactComponent = testTarget.charAt(0).toUpperCase() === testTarget.charAt(0); let imports = ''; let testSetup = ''; let testBody = ''; switch (framework) { case 'jest': if (isReactComponent && testTypes.includes('unit')) { imports = `import React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport ${testTarget} from '../src/components/${testTarget}';\n\n`; testSetup = `describe('${testTarget} Component', () => {\n`; // Group test cases by type const unitTests = testCases.filter(tc => tc.type === 'unit'); unitTests.forEach(tc => { testBody += ` test('${tc.description}', () => {\n`; testBody += ` // Scenario: ${tc.scenario}\n`; testBody += ` // Expected: ${tc.expectedResult}\n`; testBody += ` render(<${testTarget} />);\n`; if (tc.description.toLowerCase().includes('render')) { testBody += ` expect(screen.getByTestId('${testTarget.toLowerCase()}')).toBeInTheDocument();\n`; } else if (tc.description.toLowerCase().includes('snapshot')) { testBody += ` const { container } = render(<${testTarget} />);\n`; testBody += ` expect(container).toMatchSnapshot();\n`; } else { testBody += ` // TODO: Add assertions based on acceptance criteria\n`; } testBody += ` });\n\n`; }); } else { // Non-React module imports = `import ${testTarget} from '../src/${testTarget}';\n\n`; testSetup = `describe('${testTarget}', () => {\n`; testCases.forEach(tc => { testBody += ` test('${tc.description}', () => {\n`; testBody += ` // Scenario: ${tc.scenario}\n`; testBody += ` // Expected: ${tc.expectedResult}\n`; testBody += ` // TODO: Implement test\n`; testBody += ` expect(true).toBe(true); // Placeholder assertion\n`; testBody += ` });\n\n`; }); } testBody += `});\n`; break; case 'cypress': testSetup = `describe('${testTarget}', () => {\n`; testCases.forEach(tc => { testBody += ` it('${tc.description}', () => {\n`; testBody += ` // Scenario: ${tc.scenario}\n`; testBody += ` // Expected: ${tc.expectedResult}\n`; testBody += ` cy.visit('/');\n`; testBody += ` // TODO: Add Cypress commands\n`; testBody += ` });\n\n`; }); testBody += `});\n`; break; case 'playwright': imports = `import { test, expect } from '@playwright/test';\n\n`; testSetup = `test.describe('${testTarget}', () => {\n`; testCases.forEach(tc => { testBody += ` test('${tc.description}', async ({ page }) => {\n`; testBody += ` // Scenario: ${tc.scenario}\n`; testBody += ` // Expected: ${tc.expectedResult}\n`; testBody += ` await page.goto('/');\n`; testBody += ` // TODO: Add Playwright commands\n`; testBody += ` });\n\n`; }); testBody += `});\n`; break; default: // Default to Jest imports = `import ${testTarget} from '../src/${testTarget}';\n\n`; testSetup = `describe('${testTarget}', () => {\n`; testCases.forEach(tc => { testBody += ` test('${tc.description}', () => {\n`; testBody += ` // TODO: Implement test for: ${tc.description}\n`; testBody += ` expect(true).toBe(true); // Placeholder assertion\n`; testBody += ` });\n\n`; }); testBody += `});\n`; } return imports + testSetup + testBody; }