UNPKG

zai-mcp-server

Version:

🚀 REVOLUTIONARY AI-to-AI Collaboration Platform v6.1! NEW: Advanced Debugging Tools with Screenshot Analysis, Console Error Parsing, Automated Fix Generation, 5 Specialized Debugging Agents, Visual UI Analysis, JavaScript Error Intelligence, CSS/HTML Fix

550 lines (473 loc) • 15.5 kB
import { EventEmitter } from 'events'; import { exec } from 'child_process'; import { promisify } from 'util'; import { existsSync } from 'fs'; import { join } from 'path'; const execAsync = promisify(exec); /** * Quality Assurance Integration for AI-to-AI Communication * Handles automated testing and code quality metrics */ export class QualityAssurance extends EventEmitter { constructor(workspaceRoot = process.cwd()) { super(); this.workspaceRoot = workspaceRoot; this.testResults = new Map(); // loopId -> test results this.qualityMetrics = new Map(); // loopId -> quality metrics this.testCommands = this.detectTestCommands(); this.lintCommands = this.detectLintCommands(); } /** * Detect available test commands * @returns {Array} - Array of test commands */ detectTestCommands() { const commands = []; const packageJsonPath = join(this.workspaceRoot, 'package.json'); if (existsSync(packageJsonPath)) { try { const packageJson = require(packageJsonPath); const scripts = packageJson.scripts || {}; // Common test script names const testScripts = ['test', 'test:unit', 'test:integration', 'jest', 'mocha', 'vitest']; testScripts.forEach(script => { if (scripts[script]) { commands.push({ name: script, command: `npm run ${script}`, type: 'npm' }); } }); // Check for direct test runners if (packageJson.devDependencies || packageJson.dependencies) { const deps = { ...packageJson.devDependencies, ...packageJson.dependencies }; if (deps.jest) { commands.push({ name: 'jest', command: 'npx jest', type: 'direct' }); } if (deps.mocha) { commands.push({ name: 'mocha', command: 'npx mocha', type: 'direct' }); } if (deps.vitest) { commands.push({ name: 'vitest', command: 'npx vitest run', type: 'direct' }); } } } catch (error) { console.error('[QA] Error reading package.json:', error.message); } } return commands; } /** * Detect available lint commands * @returns {Array} - Array of lint commands */ detectLintCommands() { const commands = []; const packageJsonPath = join(this.workspaceRoot, 'package.json'); if (existsSync(packageJsonPath)) { try { const packageJson = require(packageJsonPath); const scripts = packageJson.scripts || {}; // Common lint script names const lintScripts = ['lint', 'lint:js', 'lint:css', 'eslint', 'stylelint']; lintScripts.forEach(script => { if (scripts[script]) { commands.push({ name: script, command: `npm run ${script}`, type: 'npm' }); } }); // Check for direct linters if (packageJson.devDependencies || packageJson.dependencies) { const deps = { ...packageJson.devDependencies, ...packageJson.dependencies }; if (deps.eslint) { commands.push({ name: 'eslint', command: 'npx eslint .', type: 'direct' }); } if (deps.stylelint) { commands.push({ name: 'stylelint', command: 'npx stylelint "**/*.css"', type: 'direct' }); } } } catch (error) { console.error('[QA] Error reading package.json for lint commands:', error.message); } } return commands; } /** * Run tests after implementation * @param {string} loopId - Loop ID * @param {Array} changedFiles - Files that were changed * @returns {Promise<Object>} - Test results */ async runTestsAfterImplementation(loopId, changedFiles = []) { console.error(`[QA] Running tests for loop ${loopId}`); const results = { timestamp: new Date(), success: false, tests: [], summary: { total: 0, passed: 0, failed: 0, skipped: 0 }, coverage: null, duration: 0, errors: [] }; const startTime = Date.now(); try { // Run available test commands for (const testCmd of this.testCommands.slice(0, 2)) { // Limit to 2 commands try { console.error(`[QA] Running: ${testCmd.command}`); const { stdout, stderr } = await execAsync(testCmd.command, { cwd: this.workspaceRoot, timeout: 60000 // 1 minute timeout }); const testResult = this.parseTestOutput(testCmd.name, stdout, stderr); results.tests.push(testResult); // Update summary results.summary.total += testResult.total; results.summary.passed += testResult.passed; results.summary.failed += testResult.failed; results.summary.skipped += testResult.skipped; } catch (error) { console.error(`[QA] Test command failed: ${testCmd.command}`, error.message); results.errors.push({ command: testCmd.command, error: error.message }); } } results.success = results.summary.failed === 0 && results.summary.total > 0; results.duration = Date.now() - startTime; // Store results this.testResults.set(loopId, results); this.emit('testsCompleted', { loopId, results, changedFiles }); } catch (error) { results.errors.push({ error: error.message }); console.error(`[QA] Error running tests: ${error.message}`); } return results; } /** * Parse test output * @param {string} testRunner - Test runner name * @param {string} stdout - Standard output * @param {string} stderr - Standard error * @returns {Object} - Parsed test result */ parseTestOutput(testRunner, stdout, stderr) { const result = { runner: testRunner, total: 0, passed: 0, failed: 0, skipped: 0, coverage: null, output: stdout, errors: stderr }; const output = stdout + stderr; // Jest parsing if (testRunner.includes('jest')) { const testMatch = output.match(/Tests:\s*(\d+)\s*failed,\s*(\d+)\s*passed,\s*(\d+)\s*total/); if (testMatch) { result.failed = parseInt(testMatch[1]); result.passed = parseInt(testMatch[2]); result.total = parseInt(testMatch[3]); } // Coverage const coverageMatch = output.match(/All files\s*\|\s*([\d.]+)/); if (coverageMatch) { result.coverage = parseFloat(coverageMatch[1]); } } // Mocha parsing if (testRunner.includes('mocha')) { const passMatch = output.match(/(\d+)\s*passing/); const failMatch = output.match(/(\d+)\s*failing/); if (passMatch) {result.passed = parseInt(passMatch[1]);} if (failMatch) {result.failed = parseInt(failMatch[1]);} result.total = result.passed + result.failed; } // Vitest parsing if (testRunner.includes('vitest')) { const testMatch = output.match(/Test Files\s*(\d+)\s*passed/); if (testMatch) { result.passed = parseInt(testMatch[1]); result.total = result.passed; } } return result; } /** * Measure code quality metrics * @param {string} loopId - Loop ID * @param {Object} beforeState - State before changes * @param {Object} afterState - State after changes * @returns {Promise<Object>} - Quality metrics */ async measureCodeQuality(loopId, beforeState = null, afterState = null) { console.error(`[QA] Measuring code quality for loop ${loopId}`); const metrics = { timestamp: new Date(), linting: await this.runLinting(), complexity: await this.analyzeComplexity(), maintainability: await this.analyzeMaintainability(), performance: await this.analyzePerformance(), security: await this.analyzeSecurity(), improvement: this.calculateImprovement(beforeState, afterState) }; this.qualityMetrics.set(loopId, metrics); this.emit('qualityMeasured', { loopId, metrics }); return metrics; } /** * Run linting * @returns {Promise<Object>} - Linting results */ async runLinting() { const results = { success: false, errors: 0, warnings: 0, issues: [], tools: [] }; for (const lintCmd of this.lintCommands.slice(0, 2)) { // Limit to 2 commands try { console.error(`[QA] Running lint: ${lintCmd.command}`); const { stdout, stderr } = await execAsync(lintCmd.command, { cwd: this.workspaceRoot, timeout: 30000 // 30 seconds timeout }); const lintResult = this.parseLintOutput(lintCmd.name, stdout, stderr); results.tools.push(lintResult); results.errors += lintResult.errors; results.warnings += lintResult.warnings; results.issues.push(...lintResult.issues); } catch (error) { // Linting often exits with non-zero code when issues found if (error.stdout || error.stderr) { const lintResult = this.parseLintOutput(lintCmd.name, error.stdout || '', error.stderr || ''); results.tools.push(lintResult); results.errors += lintResult.errors; results.warnings += lintResult.warnings; } } } results.success = results.errors === 0; return results; } /** * Parse lint output * @param {string} linter - Linter name * @param {string} stdout - Standard output * @param {string} stderr - Standard error * @returns {Object} - Parsed lint result */ parseLintOutput(linter, stdout, stderr) { const result = { linter, errors: 0, warnings: 0, issues: [] }; const output = stdout + stderr; // ESLint parsing if (linter.includes('eslint')) { const lines = output.split('\n'); lines.forEach(line => { if (line.includes('error')) {result.errors++;} if (line.includes('warning')) {result.warnings++;} // Extract issue details const issueMatch = line.match(/(.+):(\d+):(\d+):\s*(error|warning)\s*(.+)/); if (issueMatch) { result.issues.push({ file: issueMatch[1], line: parseInt(issueMatch[2]), column: parseInt(issueMatch[3]), severity: issueMatch[4], message: issueMatch[5] }); } }); } return result; } /** * Analyze code complexity * @returns {Promise<Object>} - Complexity analysis */ async analyzeComplexity() { // This would integrate with complexity analysis tools // For now, return a placeholder return { average: 5.2, max: 15, files: [], trend: 'stable' }; } /** * Analyze maintainability * @returns {Promise<Object>} - Maintainability analysis */ async analyzeMaintainability() { // This would analyze code maintainability metrics return { score: 7.8, factors: { duplication: 'low', coupling: 'medium', cohesion: 'high' } }; } /** * Analyze performance * @returns {Promise<Object>} - Performance analysis */ async analyzePerformance() { // This would run performance analysis return { score: 8.5, metrics: { bundleSize: 'optimal', loadTime: 'fast', memoryUsage: 'efficient' } }; } /** * Analyze security * @returns {Promise<Object>} - Security analysis */ async analyzeSecurity() { // This would run security analysis return { score: 9.0, vulnerabilities: 0, warnings: 1, recommendations: ['Update dependencies'] }; } /** * Calculate improvement between states * @param {Object} beforeState - Before state * @param {Object} afterState - After state * @returns {Object} - Improvement calculation */ calculateImprovement(beforeState, afterState) { if (!beforeState || !afterState) { return { available: false }; } return { available: true, testCoverage: this.calculateDelta(beforeState.coverage, afterState.coverage), lintErrors: this.calculateDelta(beforeState.lintErrors, afterState.lintErrors, true), complexity: this.calculateDelta(beforeState.complexity, afterState.complexity, true), performance: this.calculateDelta(beforeState.performance, afterState.performance) }; } /** * Calculate delta between two values * @param {number} before - Before value * @param {number} after - After value * @param {boolean} lowerIsBetter - Whether lower values are better * @returns {Object} - Delta calculation */ calculateDelta(before, after, lowerIsBetter = false) { if (before === undefined || after === undefined) { return { change: 0, direction: 'unchanged' }; } const change = after - before; let direction = 'unchanged'; if (change > 0) { direction = lowerIsBetter ? 'worse' : 'better'; } else if (change < 0) { direction = lowerIsBetter ? 'better' : 'worse'; } return { change: Math.abs(change), direction, percentage: before !== 0 ? Math.abs((change / before) * 100) : 0 }; } /** * Generate quality report * @param {string} loopId - Loop ID * @returns {Object} - Quality report */ generateQualityReport(loopId) { const testResults = this.testResults.get(loopId); const qualityMetrics = this.qualityMetrics.get(loopId); const report = { loopId, timestamp: new Date(), overall: 'unknown', tests: testResults || null, quality: qualityMetrics || null, recommendations: [] }; // Calculate overall score let score = 0; let factors = 0; if (testResults) { if (testResults.success) {score += 30;} factors++; } if (qualityMetrics) { if (qualityMetrics.linting.success) {score += 20;} score += (qualityMetrics.complexity.average < 10 ? 15 : 5); score += (qualityMetrics.maintainability.score * 2); score += (qualityMetrics.performance.score * 2); score += (qualityMetrics.security.score * 1); factors += 5; } const overallScore = factors > 0 ? score / factors : 0; if (overallScore >= 80) {report.overall = 'excellent';} else if (overallScore >= 60) {report.overall = 'good';} else if (overallScore >= 40) {report.overall = 'fair';} else {report.overall = 'poor';} // Generate recommendations if (testResults && !testResults.success) { report.recommendations.push('Fix failing tests before proceeding'); } if (qualityMetrics && qualityMetrics.linting.errors > 0) { report.recommendations.push('Address linting errors'); } if (qualityMetrics && qualityMetrics.complexity.average > 10) { report.recommendations.push('Reduce code complexity'); } return report; } /** * Get test results for loop * @param {string} loopId - Loop ID * @returns {Object|null} - Test results */ getTestResults(loopId) { return this.testResults.get(loopId) || null; } /** * Get quality metrics for loop * @param {string} loopId - Loop ID * @returns {Object|null} - Quality metrics */ getQualityMetrics(loopId) { return this.qualityMetrics.get(loopId) || null; } }