UNPKG

vineguard-mcp-server-standalone

Version:

VineGuard MCP Server v2.1 - Intelligent QA Workflow System with advanced test generation for Jest/RTL, Cypress, and Playwright. Features smart project analysis, progressive testing strategies, and comprehensive quality patterns for React/Vue/Angular proje

495 lines 19.5 kB
/** * Configuration Analyzer for VineGuard * Deep analysis and optimization of testing framework configurations */ import fs from 'fs/promises'; import path from 'path'; export class ConfigAnalyzer { projectRoot; constructor(projectRoot) { this.projectRoot = projectRoot; } /** * Analyze all testing framework configurations */ async analyzeAllConfigs() { const configFiles = [ { file: 'jest.config.js', framework: 'jest' }, { file: 'jest.config.ts', framework: 'jest' }, { file: 'jest.config.json', framework: 'jest' }, { file: 'playwright.config.ts', framework: 'playwright' }, { file: 'playwright.config.js', framework: 'playwright' }, { file: 'cypress.config.js', framework: 'cypress' }, { file: 'cypress.config.ts', framework: 'cypress' }, { file: 'vitest.config.ts', framework: 'vitest' }, { file: 'vitest.config.js', framework: 'vitest' }, { file: 'vite.config.ts', framework: 'vitest' }, // Vitest can be in vite config ]; const analyses = []; for (const { file, framework } of configFiles) { const analysis = await this.analyzeConfig(file, framework); if (analysis.exists || await this.shouldHaveConfig(framework)) { analyses.push(analysis); } } return analyses; } /** * Analyze a specific configuration file */ async analyzeConfig(filename, framework) { const filePath = path.join(this.projectRoot, filename); const analysis = { file: filename, framework, exists: false, valid: false, score: 0, issues: [], optimizations: [], bestPractices: [] }; try { await fs.access(filePath); analysis.exists = true; const content = await fs.readFile(filePath, 'utf8'); await this.analyzeConfigContent(analysis, content); } catch (error) { analysis.exists = false; // Check if this framework should have a config if (await this.shouldHaveConfig(framework)) { analysis.issues.push({ severity: 'warning', message: `Missing ${framework} configuration file`, suggestion: `Create ${filename} for optimal ${framework} configuration` }); } } // Calculate overall score analysis.score = this.calculateConfigScore(analysis); return analysis; } /** * Analyze configuration file content */ async analyzeConfigContent(analysis, content) { try { let config; // Parse configuration based on file type if (analysis.file.endsWith('.json')) { config = JSON.parse(content); } else { // For .js and .ts files, do pattern matching config = this.extractConfigFromCode(content); } analysis.valid = true; // Framework-specific analysis switch (analysis.framework) { case 'jest': this.analyzeJestConfig(analysis, config, content); break; case 'playwright': this.analyzePlaywrightConfig(analysis, config, content); break; case 'cypress': this.analyzeCypressConfig(analysis, config, content); break; case 'vitest': this.analyzeVitestConfig(analysis, config, content); break; } } catch (error) { analysis.valid = false; analysis.issues.push({ severity: 'error', message: `Failed to parse configuration: ${error instanceof Error ? error.message : 'Unknown error'}`, suggestion: 'Fix syntax errors in configuration file' }); } } /** * Analyze Jest configuration */ analyzeJestConfig(analysis, config, content) { // Check for test environment if (!this.hasConfigField(config, content, 'testEnvironment')) { analysis.issues.push({ severity: 'warning', message: 'testEnvironment not specified', field: 'testEnvironment', suggestion: 'Specify testEnvironment (jsdom for browser, node for server)' }); } // Check for coverage configuration if (!this.hasConfigField(config, content, 'collectCoverageFrom')) { analysis.optimizations.push({ type: 'coverage', description: 'Configure coverage collection patterns', implementation: 'Add collectCoverageFrom array with file patterns', impact: 'high' }); } // Check for coverage thresholds if (!this.hasConfigField(config, content, 'coverageThreshold')) { analysis.optimizations.push({ type: 'coverage', description: 'Set coverage thresholds for quality gates', implementation: 'Add coverageThreshold with branch, function, line, statement targets', impact: 'high' }); } // Check for setup files if (!this.hasConfigField(config, content, 'setupFilesAfterEnv')) { analysis.issues.push({ severity: 'info', message: 'No setup files configured', suggestion: 'Consider adding setupFilesAfterEnv for global test setup' }); } // Check for module name mapping (common in React projects) if (!this.hasConfigField(config, content, 'moduleNameMapping')) { analysis.optimizations.push({ type: 'maintainability', description: 'Configure module name mapping for assets', implementation: 'Add moduleNameMapping for CSS and asset imports', impact: 'medium' }); } // Best practices analysis.bestPractices.push({ category: 'Performance', description: 'Use maxWorkers for parallel execution', implemented: this.hasConfigField(config, content, 'maxWorkers'), recommendation: 'Set maxWorkers to optimize test execution speed' }, { category: 'Debugging', description: 'Configure verbose output for CI', implemented: this.hasConfigField(config, content, 'verbose'), recommendation: 'Set verbose: true in CI environments' }, { category: 'Isolation', description: 'Clear mocks between tests', implemented: this.hasConfigField(config, content, 'clearMocks'), recommendation: 'Enable clearMocks for better test isolation' }); } /** * Analyze Playwright configuration */ analyzePlaywrightConfig(analysis, config, content) { // Check for base URL if (!this.hasConfigField(config, content, 'baseURL')) { analysis.issues.push({ severity: 'warning', message: 'baseURL not configured', field: 'baseURL', suggestion: 'Set baseURL for consistent test execution' }); } // Check for projects (multi-browser testing) if (!this.hasConfigField(config, content, 'projects')) { analysis.optimizations.push({ type: 'coverage', description: 'Configure multi-browser testing', implementation: 'Add projects array with different browser configurations', impact: 'high' }); } // Check for web server configuration if (!this.hasConfigField(config, content, 'webServer')) { analysis.issues.push({ severity: 'info', message: 'webServer not configured', suggestion: 'Configure webServer to automatically start development server' }); } // Check for trace configuration if (!this.hasConfigField(config, content, 'trace')) { analysis.optimizations.push({ type: 'maintainability', description: 'Enable trace for debugging failures', implementation: 'Set trace: "on-first-retry" for debugging', impact: 'medium' }); } // Best practices analysis.bestPractices.push({ category: 'Performance', description: 'Configure parallel execution', implemented: this.hasConfigField(config, content, 'fullyParallel'), recommendation: 'Enable fullyParallel for faster test execution' }, { category: 'Reliability', description: 'Configure retries for flaky tests', implemented: this.hasConfigField(config, content, 'retries'), recommendation: 'Set retries for CI environments to handle flaky tests' }, { category: 'Reporting', description: 'Configure HTML reporter', implemented: this.hasConfigField(config, content, 'reporter'), recommendation: 'Use HTML reporter for detailed test results' }); } /** * Analyze Cypress configuration */ analyzeCypressConfig(analysis, config, content) { // Check for base URL if (!this.hasConfigField(config, content, 'baseUrl')) { analysis.issues.push({ severity: 'warning', message: 'baseUrl not configured', field: 'baseUrl', suggestion: 'Set baseUrl for consistent test execution' }); } // Check for E2E configuration if (!this.hasConfigField(config, content, 'e2e')) { analysis.issues.push({ severity: 'warning', message: 'e2e configuration missing', suggestion: 'Configure e2e settings for end-to-end testing' }); } // Check for component testing if (!this.hasConfigField(config, content, 'component')) { analysis.optimizations.push({ type: 'coverage', description: 'Configure component testing', implementation: 'Add component testing configuration', impact: 'medium' }); } // Best practices analysis.bestPractices.push({ category: 'Performance', description: 'Configure video recording selectively', implemented: this.hasConfigField(config, content, 'video'), recommendation: 'Disable video in development, enable in CI' }, { category: 'Debugging', description: 'Configure screenshot capture', implemented: this.hasConfigField(config, content, 'screenshotOnRunFailure'), recommendation: 'Enable screenshots on failure for debugging' }); } /** * Analyze Vitest configuration */ analyzeVitestConfig(analysis, config, content) { // Check for test environment if (!this.hasConfigField(config, content, 'environment')) { analysis.issues.push({ severity: 'warning', message: 'test environment not specified', suggestion: 'Specify test environment (jsdom for browser-like, node for server)' }); } // Check for coverage configuration if (!this.hasConfigField(config, content, 'coverage')) { analysis.optimizations.push({ type: 'coverage', description: 'Configure code coverage reporting', implementation: 'Add coverage configuration with provider and thresholds', impact: 'high' }); } // Best practices analysis.bestPractices.push({ category: 'Performance', description: 'Enable globals for easier testing', implemented: this.hasConfigField(config, content, 'globals'), recommendation: 'Enable globals to avoid importing test functions' }, { category: 'Developer Experience', description: 'Configure CSS handling', implemented: this.hasConfigField(config, content, 'css'), recommendation: 'Enable CSS processing for component tests' }); } /** * Check if a configuration field exists */ hasConfigField(config, content, field) { // First check parsed config object if (config && this.deepHasProperty(config, field)) { return true; } // Fallback to string search in content return content.includes(field); } /** * Deep property check for nested config objects */ deepHasProperty(obj, path) { if (!obj || typeof obj !== 'object') return false; const keys = path.split('.'); let current = obj; for (const key of keys) { if (current[key] === undefined) return false; current = current[key]; } return true; } /** * Extract configuration from JavaScript/TypeScript code */ extractConfigFromCode(content) { try { // Simple pattern matching for common config patterns const config = {}; // Look for common patterns const patterns = [ /testEnvironment:\s*['"]([^'"]+)['"]/, /baseURL:\s*['"]([^'"]+)['"]/, /baseUrl:\s*['"]([^'"]+)['"]/, /coverageThreshold:/, /collectCoverageFrom:/, /projects:/, /webServer:/, /retries:/, /fullyParallel:/ ]; for (const pattern of patterns) { if (pattern.test(content)) { // Extract the configuration key const match = content.match(pattern); if (match) { const key = match[0].split(':')[0].trim(); config[key] = true; // Mark as present } } } return config; } catch (error) { return {}; } } /** * Check if a framework should have a configuration file */ async shouldHaveConfig(framework) { try { const packageJsonPath = path.join(this.projectRoot, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; switch (framework) { case 'jest': return 'jest' in allDeps; case 'playwright': return '@playwright/test' in allDeps || 'playwright' in allDeps; case 'cypress': return 'cypress' in allDeps; case 'vitest': return 'vitest' in allDeps; default: return false; } } catch (error) { return false; } } /** * Calculate configuration quality score */ calculateConfigScore(analysis) { if (!analysis.exists) return 0; if (!analysis.valid) return 10; let score = 50; // Base score for existing, valid config // Deduct points for issues for (const issue of analysis.issues) { switch (issue.severity) { case 'error': score -= 15; break; case 'warning': score -= 10; break; case 'info': score -= 5; break; } } // Add points for implemented best practices const implementedPractices = analysis.bestPractices.filter(bp => bp.implemented).length; const totalPractices = analysis.bestPractices.length; if (totalPractices > 0) { score += (implementedPractices / totalPractices) * 30; } // Add points for optimization opportunities (indicates thorough analysis) if (analysis.optimizations.length === 0) { score += 20; // Well-optimized config } return Math.max(0, Math.min(100, Math.round(score))); } /** * Generate optimization recommendations */ async generateOptimizationReport(analyses) { const validConfigs = analyses.filter(a => a.valid); const totalScore = validConfigs.reduce((sum, a) => sum + a.score, 0); const criticalIssues = analyses.reduce((sum, a) => sum + a.issues.filter(i => i.severity === 'error').length, 0); // Prioritize optimizations const allOptimizations = analyses.flatMap(a => a.optimizations.map(opt => ({ ...opt, config: a.file, framework: a.framework }))); const prioritizedActions = this.prioritizeOptimizations(allOptimizations); // Identify best practice gaps const bestPracticeGaps = this.identifyBestPracticeGaps(analyses); return { summary: { totalConfigs: analyses.length, validConfigs: validConfigs.length, averageScore: validConfigs.length > 0 ? Math.round(totalScore / validConfigs.length) : 0, criticalIssues }, prioritizedActions, bestPracticeGaps }; } prioritizeOptimizations(optimizations) { const priorities = { high: 3, medium: 2, low: 1 }; return optimizations .sort((a, b) => (priorities[b.impact] || 1) - (priorities[a.impact] || 1)) .map(opt => ({ priority: opt.impact, description: opt.description, implementation: opt.implementation, affectedConfigs: [opt.config] })); } identifyBestPracticeGaps(analyses) { const gaps = new Map(); for (const analysis of analyses) { for (const practice of analysis.bestPractices) { if (!practice.implemented) { const key = `${practice.category}-${practice.description}`; if (gaps.has(key)) { gaps.get(key).configs.push(analysis.file); } else { gaps.set(key, { category: practice.category, description: practice.description, recommendation: practice.recommendation || 'Consider implementing this best practice', configs: [analysis.file] }); } } } } return Array.from(gaps.values()); } } //# sourceMappingURL=config-analyzer.js.map