@wernerthiago/teo
Version:
Test Execution Optimizer
296 lines (258 loc) • 9.95 kB
JavaScript
/**
* TEO Core Engine - Refactored for Path-Based Approach
*
* Main orchestrator that coordinates analysis and test selection
* without direct test execution.
*/
import GitDiffAnalyzer from '../analyzers/git-analyzer.js'
import FeatureMapper from '../mappers/feature-mapper.js'
import ProviderManager from '../providers/provider-manager.js'
import TestOrchestrator from '../integrations/test-orchestrator.js'
import logger from './logger.js'
export class TEOEngine {
constructor(config) {
this.config = config
this.repoPath = config.get('repo_path', process.cwd())
// Initialize components
this.gitAnalyzer = new GitDiffAnalyzer(this.repoPath)
this.featureMapper = new FeatureMapper(this.repoPath, config.config)
this.providerManager = new ProviderManager(config.get('ai_providers', {}))
this.testOrchestrator = new TestOrchestrator(config.config)
logger.info('TEOEngine initialized', {
repoPath: this.repoPath,
aiProviders: this.providerManager.getAvailableProviders(),
frameworks: this.testOrchestrator.getAvailableFrameworks()
})
}
/**
* Analyze code changes and select relevant tests
* @param {string} baseRef - Base git reference
* @param {string} headRef - Head git reference
* @param {Object} options - Analysis options
* @returns {Promise<Object>} Analysis result with selected tests
*/
async analyze(baseRef, headRef, options = {}) {
const startTime = Date.now()
try {
logger.info('Starting TEO analysis', { baseRef, headRef, options })
// Step 1: Analyze git diff
const diffAnalysis = await this.gitAnalyzer.analyzeDiff(baseRef, headRef)
logger.info('Git diff analysis completed', {
filesChanged: diffAnalysis.changes?.length || 0,
languages: Array.from(diffAnalysis.languagesAffected || [])
})
// Debug: Log the diffAnalysis structure
logger.debug('Diff analysis result', {
hasChanges: !!diffAnalysis.changes,
changesLength: diffAnalysis.changes?.length,
diffAnalysisKeys: Object.keys(diffAnalysis)
})
// Step 2: Map changes to features
const featureMapping = await this.featureMapper.mapChangesToFeatures(diffAnalysis)
logger.info('Feature mapping completed', {
featuresDetected: featureMapping.length
})
// Step 3: AI-enhanced analysis (if enabled)
let aiAnalysis = null
if (options.useAI !== false && featureMapping.length > 0) {
logger.info('Starting AI analysis', { options: options });
try {
aiAnalysis = await this.enhanceWithAI(diffAnalysis, featureMapping)
logger.info('AI analysis completed')
} catch (error) {
logger.warn('AI analysis failed, continuing without AI enhancement', { error: error.message })
}
}
// Step 4: Select tests for specified framework
const framework = options.framework || 'playwright'
let testSelection = null
if (featureMapping.length > 0) {
testSelection = await this.testOrchestrator.selectTests(framework, featureMapping)
logger.info('Test selection completed', {
framework,
testsSelected: testSelection.selectedTests.length,
totalAvailable: testSelection.summary.totalAvailable
})
} else {
logger.info('No features detected, no tests selected')
testSelection = {
framework,
selectedTests: [],
selectionReasons: [],
summary: {
totalAvailable: 0,
totalSelected: 0,
reductionPercentage: 0
}
}
}
const duration = Date.now() - startTime
const result = {
analysis: {
baseRef,
headRef,
timestamp: new Date().toISOString(),
duration
},
diffAnalysis,
featureMapping,
aiAnalysis,
testSelection,
summary: {
filesChanged: diffAnalysis.changes.length,
featuresDetected: featureMapping.length,
testsSelected: testSelection.selectedTests.length,
framework,
reductionPercentage: testSelection.summary.reductionPercentage,
estimatedTimeSaved: this.calculateTimeSaved(testSelection.summary),
duration
}
}
logger.info('TEO analysis completed', result.summary)
return result
} catch (error) {
logger.error('TEO analysis failed', { error: error.message, stack: error.stack })
throw error
}
}
/**
* Generate output in specified format
* @param {Object} analysisResult - Result from analyze()
* @param {string} format - Output format
* @returns {string} Formatted output
*/
generateOutput(analysisResult, format = 'paths') {
if (!analysisResult.testSelection) {
throw new Error('No test selection data available')
}
return this.testOrchestrator.generateOutput(analysisResult.testSelection, format)
}
/**
* Enhance analysis with AI
* @param {Object} diffAnalysis - Git diff analysis result
* @param {Object[]} featureMapping - Feature mapping result
* @param {Object} options - AI options
* @returns {Promise<Object>} AI analysis result
*/
async enhanceWithAI(diffAnalysis, featureMapping, options = {}) {
const provider = this.providerManager.primaryProvider
logger.info('Provider selected for AI analysis', {
provider: provider,
})
const prompt = this.buildAIPrompt(diffAnalysis, featureMapping)
const aiResult = await provider.analyzeImpact(prompt, {
maxTokens: options.maxTokens || 2000,
temperature: options.temperature || 0.1
})
return {
provider: provider.constructor.name,
confidence: aiResult.confidence || 0.8,
reasoning: aiResult.reasoning,
recommendations: aiResult.recommendations || [],
enhancedFeatures: this.enhanceFeatures(featureMapping, aiResult)
}
}
/**
* Build AI prompt for impact analysis
* @param {Object} diffAnalysis - Git diff analysis
* @param {Object[]} featureMapping - Feature mapping
* @returns {string} AI prompt
*/
buildAIPrompt(diffAnalysis, featureMapping) {
const changedFiles = diffAnalysis.changes.map(c => c.filePath).join(', ')
const features = featureMapping.map(f => f.featureName).join(', ')
return `Analyze the impact of code changes on test requirements:
Changed Files: ${changedFiles}
Detected Features: ${features}
Languages: ${Array.from(diffAnalysis.languagesAffected).join(', ')}
Please assess:
1. Which features are most likely impacted by these changes?
2. What types of tests should be prioritized?
3. Are there any cross-feature dependencies to consider?
4. Risk level of these changes (low/medium/high)?
Provide a structured analysis with confidence scores.`
}
/**
* Enhance feature mapping with AI insights
* @param {Object[]} features - Original feature mapping
* @param {Object} aiResult - AI analysis result
* @returns {Object[]} Enhanced features
*/
enhanceFeatures(features, aiResult) {
return features.map(feature => ({
...feature,
aiConfidence: aiResult.confidence,
aiReasoning: aiResult.reasoning,
enhancedConfidence: Math.min(feature.confidence + (aiResult.confidence * 0.2), 1.0)
}))
}
/**
* Calculate estimated time saved
* @param {Object} summary - Test selection summary
* @returns {number} Estimated time saved in milliseconds
*/
calculateTimeSaved(summary) {
// Rough estimate: assume each test takes 5 seconds on average
const avgTestTime = 5000
const testsSaved = summary.totalAvailable - summary.totalSelected
return testsSaved * avgTestTime
}
/**
* Validate TEO setup and configuration
* @returns {Promise<Object>} Validation result
*/
async validate() {
const result = {
overall: { valid: true, errors: [], warnings: [] },
git: { valid: true, errors: [], warnings: [] },
config: { valid: true, errors: [], warnings: [] },
aiProviders: {},
frameworks: {}
}
try {
// Validate git repository
try {
await this.gitAnalyzer.validateRepository()
result.git.info = 'Git repository is valid'
} catch (error) {
result.git.valid = false
result.git.errors.push(error.message)
result.overall.valid = false
}
// Validate configuration
const configValidation = this.config.validate()
result.config = configValidation
if (!configValidation.valid) {
result.overall.valid = false
}
// Validate AI providers
const providers = this.providerManager.getAvailableProviders()
for (const providerName of providers) {
try {
const provider = await this.providerManager.getProvider(providerName)
const validation = await provider.validateConnection()
result.aiProviders[providerName] = validation
} catch (error) {
result.aiProviders[providerName] = {
valid: false,
errors: [error.message]
}
result.overall.warnings.push(`AI provider ${providerName} validation failed`)
}
}
// Validate test frameworks
result.frameworks = await this.testOrchestrator.validateAll()
for (const [framework, validation] of Object.entries(result.frameworks)) {
if (!validation.valid) {
result.overall.warnings.push(`Framework ${framework} validation failed`)
}
}
} catch (error) {
result.overall.valid = false
result.overall.errors.push(`Validation failed: ${error.message}`)
}
logger.info('TEO validation completed', result.overall)
return result
}
}
export default TEOEngine