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
JavaScript
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