UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

365 lines 16.3 kB
export class TestReviewerAI { options; constructor(options = {}) { this.options = { model: 'gpt-4', temperature: 0.1, qualityThreshold: 0.85, autoFixThreshold: 0.70, criteriaWeights: { clarity: 0.25, robustness: 0.35, maintainability: 0.25, security: 0.15 }, ...options }; } async reviewTest(testCode, framework) { try { // Validate input if (!testCode || testCode.trim().length === 0) { return this.createFailedReview('Empty test code provided', framework, testCode); } if (!this.isValidTestCode(testCode)) { return this.createFailedReview('invalid test code format', framework, testCode); } // Analyze each criterion const clarity = this.analyzeClarity(testCode); const robustness = this.analyzeRobustness(testCode); const maintainability = this.analyzeMaintainability(testCode); const security = this.analyzeSecurity(testCode); const criteria = { clarity, robustness, maintainability, security }; const overallScore = this.calculateOverallScore(criteria); const approved = overallScore >= this.options.qualityThreshold; const feedback = this.generateFeedback(criteria); const autoFixSuggestions = overallScore >= this.options.autoFixThreshold ? this.autoFix(testCode) : []; const escalateToHuman = this.shouldEscalateToHuman(testCode, overallScore); return { overallScore, criteria, feedback, approved, autoFixSuggestions, escalateToHuman, timestamp: new Date(), framework, testCode }; } catch (error) { return this.createFailedReview(`Review failed: ${error instanceof Error ? error.message : 'Unknown error'}`, framework, testCode); } } analyzeClarity(testCode) { const hasDescriptiveName = this.hasDescriptiveTestName(testCode); const explainsPurpose = this.explainsPurpose(testCode); let score = 0; if (hasDescriptiveName) score += 0.5; if (explainsPurpose) score += 0.5; return { hasDescriptiveName, explainsPurpose, score }; } analyzeRobustness(testCode) { const avoidsBrittleSelectors = this.avoidsBrittleSelectors(testCode); const handlesAsyncProperly = this.handlesAsyncProperly(testCode); let score = 0; if (avoidsBrittleSelectors) score += 0.6; // More weight on selectors if (handlesAsyncProperly) score += 0.4; return { avoidsBrittleSelectors, handlesAsyncProperly, score }; } analyzeMaintainability(testCode) { const followsConventions = this.followsConventions(testCode); const appropriateAbstractions = this.hasAppropriateAbstractions(testCode); let score = 0; if (followsConventions) score += 0.5; if (appropriateAbstractions) score += 0.5; return { followsConventions, appropriateAbstractions, score }; } analyzeSecurity(testCode) { const noSensitiveData = this.hasNoSensitiveData(testCode); const noHardcodedSecrets = this.hasNoHardcodedSecrets(testCode); let score = 0; if (noSensitiveData) score += 0.5; if (noHardcodedSecrets) score += 0.5; return { noSensitiveData, noHardcodedSecrets, score }; } calculateOverallScore(criteria) { const weights = this.options.criteriaWeights; return (criteria.clarity.score * weights.clarity + criteria.robustness.score * weights.robustness + criteria.maintainability.score * weights.maintainability + criteria.security.score * weights.security); } generateFeedback(criteria) { const feedback = []; // Check overall quality const overallScore = this.calculateOverallScore(criteria); if (overallScore >= 0.9) { feedback.push('✅ excellent test quality! This test is approved for production use.'); } else if (overallScore >= this.options.qualityThreshold) { feedback.push('✅ Good test quality. This test is approved with minor suggestions below.'); } else { feedback.push('❌ Test needs improvement before approval.'); } // Clarity feedback if (!criteria.clarity.hasDescriptiveName) { feedback.push('📝 **Clarity**: Use a more descriptive test name that explains what behavior is being tested.'); } if (!criteria.clarity.explainsPurpose) { feedback.push('📝 **Clarity**: Add comments explaining the test purpose and key assertions.'); } // Robustness feedback if (!criteria.robustness.avoidsBrittleSelectors) { feedback.push('🔧 **Robustness**: Avoid brittle selectors like CSS paths or nth-child. Use data-testid, aria-label, or semantic selectors instead.'); } if (!criteria.robustness.handlesAsyncProperly) { feedback.push('⏳ **Robustness**: Use proper async/await patterns and wait for elements before interacting.'); } // Maintainability feedback if (!criteria.maintainability.followsConventions) { feedback.push('📋 **Maintainability**: Follow testing framework conventions for imports, test structure, and naming.'); } if (!criteria.maintainability.appropriateAbstractions) { feedback.push('🔄 **Maintainability**: Consider extracting repetitive actions into reusable helper functions.'); } // Security feedback if (!criteria.security.noSensitiveData) { feedback.push('🔒 **Security**: Remove or mock sensitive data. Use environment variables or test fixtures instead.'); } if (!criteria.security.noHardcodedSecrets) { feedback.push('⚠️ **Security**: Remove hardcoded secrets, API keys, or passwords. Use environment variables or mocked values.'); } return feedback.join('\n\n'); } autoFix(testCode) { const suggestions = []; // Suggest selector improvements if (this.hasBrittleSelectors(testCode)) { suggestions.push('Replace CSS path selectors with data-testid attributes'); suggestions.push('Use semantic selectors like [aria-label="Submit"] instead of generic element selectors'); suggestions.push('Consider role-based selectors like getByRole("button", { name: "Submit" })'); } // Suggest async improvements if (!this.handlesAsyncProperly(testCode)) { suggestions.push('Add await keywords before page interactions'); suggestions.push('Use await expect() for assertions that need to wait'); suggestions.push('Consider using waitFor() patterns for dynamic content'); } // Suggest abstraction improvements if (this.hasRepetitiveCode(testCode)) { suggestions.push('Extract common actions into helper functions'); suggestions.push('Create page object models for complex interactions'); suggestions.push('Use test fixtures for common setup'); } return suggestions; } shouldEscalateToHuman(testCode, overallScore) { // Escalate if score is borderline if (overallScore >= 0.7 && overallScore < this.options.qualityThreshold) { return true; } // Escalate if test is complex if (this.isComplexTest(testCode)) { return true; } // Escalate if security issues detected if (this.hasSecurityConcerns(testCode)) { return true; } return false; } // Private helper methods isValidTestCode(code) { // Basic validation - check for test framework patterns const testPatterns = [ /test\s*\(/, /it\s*\(/, /describe\s*\(/, /@Test/, /class.*Test/ ]; return testPatterns.some(pattern => pattern.test(code)); } hasDescriptiveTestName(code) { const testNameMatch = code.match(/(?:test|it)\s*\(\s*['"`]([^'"`]+)['"`]/); if (!testNameMatch) return false; const testName = testNameMatch[1]; // Check for meaningful words and avoid generic names const meaningfulWords = ['should', 'can', 'validates', 'displays', 'redirects', 'handles']; const genericWords = ['test', 'test1', 'mytest', 'stuff', 'things']; const hasMeaningfulWords = meaningfulWords.some(word => testName.toLowerCase().includes(word)); const hasGenericWords = genericWords.some(word => testName.toLowerCase().includes(word)); return testName.length > 10 && hasMeaningfulWords && !hasGenericWords; } explainsPurpose(code) { // Look for meaningful comments that explain purpose or steps const meaningfulComments = [ /\/\/.*(?:test|validates|verifies|ensures|checks).*(?:email|format|validation|error|handling)/i, /\/\/.*(?:validates|verifies|ensures|checks|tests)/i, /\/\*.*(?:test|verify|ensure|check|validate).*\*\//i, /\/\/.*(?:should|when|given|then)/i, /\/\/.*(?:navigate|fill|select|submit|verify|click)/i, // Step-by-step comments /\/\/.*(?:shipping|payment|order|confirmation)/i // Domain-specific actions ]; // Count meaningful comment lines const lines = code.split('\n'); const meaningfulCommentCount = lines.filter(line => meaningfulComments.some(pattern => pattern.test(line))).length; // Exclude generic comments like "// some test" or "// test" only const genericComments = [ /^\/\/\s*(?:some\s+test|test)\s*$/i, /^\/\/\s*stuff\s*$/i ]; const hasGeneric = genericComments.some(pattern => pattern.test(code)); // Consider purpose explained if multiple meaningful comments or no generic ones return meaningfulCommentCount >= 2 && !hasGeneric; } avoidsBrittleSelectors(code) { return !this.hasBrittleSelectors(code); } hasBrittleSelectors(code) { const brittlePatterns = [ /nth-child\(/, /nth-of-type\(/, /body\s*>\s*div\s*>\s*div/, /'input'/g, // Generic input selector /'button'/g, // Generic button selector without attributes /\.\w+\s*>\s*\.\w+\s*>\s*\.\w+/ // Deep CSS class nesting ]; return brittlePatterns.some(pattern => pattern.test(code)); } handlesAsyncProperly(code) { // Check for proper async patterns const hasAsyncActions = /page\.(click|fill|goto|type|select)/.test(code); const hasAwaitKeywords = /await\s+page\.(click|fill|goto|type|select)/.test(code); const hasAsyncExpects = /await\s+expect/.test(code); // If no async actions, it's fine if (!hasAsyncActions) return true; // If has async actions, should have await return hasAwaitKeywords && (hasAsyncExpects || /await.*expect/.test(code)); } followsConventions(code) { // Check for proper imports const hasProperImports = /import.*from\s+['"`]@playwright\/test['"`]/.test(code) || /import.*from\s+['"`]@testing-library/.test(code) || /import.*from\s+['"`]jest['"`]/.test(code); // Check for proper test structure const hasProperStructure = /(?:test|it|describe)\s*\(/.test(code); return hasProperImports || hasProperStructure; } hasAppropriateAbstractions(code) { // Check for excessive repetition const lines = code.split('\n').filter(line => line.trim().length > 0); const lineCount = lines.length; // Look for indicators of long tests (including comments about more code) const hasMoreCodeComments = /\/\/.*\d+.*more.*lines/i.test(code); const effectiveLineCount = hasMoreCodeComments ? lineCount + 40 : lineCount; // If test is short, abstractions aren't needed if (effectiveLineCount < 15) return true; // Look for helper function calls or page object usage const hasHelperFunctions = /\w+Helper\(|\w+Page\.|loginAsUser|addItemToCart|completeCheckout/.test(code); const hasReusableFunctions = /function\s+\w+|const\s+\w+\s*=/.test(code); // Check for excessive repetitive patterns const repetitivePatterns = [ (code.match(/page\.goto\(/g) || []).length > 1, (code.match(/page\.fill\(/g) || []).length > 2, (code.match(/page\.click\(/g) || []).length > 2 ]; const hasExcessiveRepetition = repetitivePatterns.some(Boolean); // Long tests should have some abstraction, especially if repetitive if (effectiveLineCount > 15 && hasExcessiveRepetition) { return hasHelperFunctions || hasReusableFunctions; } return effectiveLineCount < 20 || hasHelperFunctions || hasReusableFunctions; } hasNoSensitiveData(code) { const sensitivePatterns = [ /user-actual-password/i, /prod-api-key/i, /real-password/i, /actual-secret/i, /production.*key/i ]; return !sensitivePatterns.some(pattern => pattern.test(code)); } hasNoHardcodedSecrets(code) { const secretPatterns = [ /['"`]sk-[a-zA-Z0-9]+['"`]/, // OpenAI API keys /['"`]secret-key-[a-zA-Z0-9]+['"`]/, // Generic secret keys /['"`][a-zA-Z0-9]{32,}['"`]/, // Long random strings (likely secrets) /api[_-]?key\s*[:=]\s*['"`][^'"`]+['"`]/i, /token\s*[:=]\s*['"`][^'"`]+['"`]/i, /password\s*[:=]\s*['"`][^'"`]+['"`]/i ]; return !secretPatterns.some(pattern => pattern.test(code)); } isComplexTest(code) { const lines = code.split('\n').length; const hasLoops = /for\s*\(|while\s*\(|forEach/.test(code); const hasConditionals = /if\s*\(|switch\s*\(/.test(code); const hasMultipleAssertions = (code.match(/expect\(/g) || []).length > 5; return lines > 50 || hasLoops || (hasConditionals && hasMultipleAssertions); } hasSecurityConcerns(code) { return !this.hasNoSensitiveData(code) || !this.hasNoHardcodedSecrets(code); } hasRepetitiveCode(code) { const lines = code.split('\n'); const uniqueLines = new Set(lines.map(line => line.trim())).size; const totalLines = lines.length; // If less than 80% of lines are unique, consider it repetitive return totalLines > 10 && (uniqueLines / totalLines) < 0.8; } createFailedReview(reason, framework, testCode) { return { overallScore: 0, criteria: { clarity: { hasDescriptiveName: false, explainsPurpose: false, score: 0 }, robustness: { avoidsBrittleSelectors: false, handlesAsyncProperly: false, score: 0 }, maintainability: { followsConventions: false, appropriateAbstractions: false, score: 0 }, security: { noSensitiveData: false, noHardcodedSecrets: false, score: 0 } }, feedback: `❌ Review failed: ${reason}. Please provide valid test code and try again.`, approved: false, autoFixSuggestions: [], escalateToHuman: true, timestamp: new Date(), framework, testCode }; } } //# sourceMappingURL=test-reviewer-ai.js.map