Skip to main content
Glama

Automation Script Generator MCP Server

smart-analyzer.js•29.7 kB
/** * Smart Analysis Module * Provides intelligent pattern recognition and suggestion capabilities */ import fs from 'fs-extra'; import path from 'path'; export class SmartAnalyzer { constructor(config, fileAnalyzer) { this.config = config; this.fileAnalyzer = fileAnalyzer; this.cache = new Map(); this.analysisHistory = []; } // Main smart analysis entry point async performSmartAnalysis(repositoryPath, scenario) { try { const cacheKey = this.generateCacheKey(repositoryPath, scenario); if (this.cache.has(cacheKey)) { console.log('Using cached smart analysis result'); return this.cache.get(cacheKey); } const analysis = await this.executeAnalysis(repositoryPath, scenario); // Cache result if configured if (this.config.get('performance.enableCaching')) { this.cache.set(cacheKey, analysis); } // Store analysis history this.analysisHistory.push({ timestamp: new Date().toISOString(), repositoryPath, scenario: scenario.scenario_title, cacheKey, patternCount: analysis.patterns?.length || 0 }); return analysis; } catch (error) { console.error('Smart analysis failed:', error); return this.getFallbackAnalysis(scenario); } } async executeAnalysis(repositoryPath, scenario) { const analysis = { timestamp: new Date().toISOString(), scenario_analysis: { title: scenario.scenario_title, complexity: this.calculateComplexity(scenario), domain: this.identifyDomain(scenario), test_type: this.classifyTestType(scenario) }, patterns: [], recommendations: [], file_suggestions: {}, reusability_score: 0, performance_insights: {} }; // Analyze existing patterns const existingPatterns = await this.analyzeExistingPatterns(repositoryPath); analysis.patterns = existingPatterns; // Generate recommendations analysis.recommendations = await this.generateRecommendations(scenario, existingPatterns); // Calculate reusability score analysis.reusability_score = this.calculateReusabilityScore(scenario, existingPatterns); // Generate file suggestions analysis.file_suggestions = await this.generateFileSuggestions(scenario, existingPatterns); // Performance insights analysis.performance_insights = this.generatePerformanceInsights(scenario, existingPatterns); return analysis; } async analyzeExistingPatterns(repositoryPath) { const patterns = []; try { // Analyze step patterns const stepPatterns = await this.analyzeStepPatterns(repositoryPath); patterns.push(...stepPatterns); // Analyze page object patterns const pagePatterns = await this.analyzePageObjectPatterns(repositoryPath); patterns.push(...pagePatterns); // Analyze selector patterns const selectorPatterns = await this.analyzeSelectorPatterns(repositoryPath); patterns.push(...selectorPatterns); // Analyze data patterns const dataPatterns = await this.analyzeDataPatterns(repositoryPath); patterns.push(...dataPatterns); } catch (error) { console.warn('Pattern analysis failed:', error.message); } return patterns; } async analyzeStepPatterns(repositoryPath) { const stepFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.steps.js'); const patterns = []; for (const file of stepFiles) { try { const content = await fs.readFile(file, 'utf8'); const stepDefinitions = this.extractStepDefinitions(content); patterns.push({ type: 'step_definition', file: path.relative(repositoryPath, file), patterns: stepDefinitions, reusability: this.calculateStepReusability(stepDefinitions), complexity: this.calculateStepComplexity(stepDefinitions) }); } catch (error) { console.warn(`Failed to analyze step file ${file}:`, error.message); } } return patterns; } async analyzePageObjectPatterns(repositoryPath) { const pageFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.page.js'); const patterns = []; for (const file of pageFiles) { try { const content = await fs.readFile(file, 'utf8'); const analysis = this.analyzePageObject(content); patterns.push({ type: 'page_object', file: path.relative(repositoryPath, file), selectors: analysis.selectors, methods: analysis.methods, inheritance: analysis.inheritance, best_practices: analysis.best_practices }); } catch (error) { console.warn(`Failed to analyze page file ${file}:`, error.message); } } return patterns; } async analyzeSelectorPatterns(repositoryPath) { const allFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.{js,ts}'); const selectorPatterns = new Map(); for (const file of allFiles) { try { const content = await fs.readFile(file, 'utf8'); const selectors = this.extractSelectors(content); selectors.forEach(selector => { const pattern = this.categorizeSelector(selector); if (!selectorPatterns.has(pattern.category)) { selectorPatterns.set(pattern.category, []); } selectorPatterns.get(pattern.category).push({ selector: selector.value, file: path.relative(repositoryPath, file), confidence: pattern.confidence }); }); } catch (error) { // Skip files that can't be read } } return Array.from(selectorPatterns.entries()).map(([category, selectors]) => ({ type: 'selector_pattern', category, examples: selectors.slice(0, 10), // Limit examples frequency: selectors.length, reliability: this.calculateSelectorReliability(selectors) })); } async analyzeDataPatterns(repositoryPath) { const dataFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.data.js'); const patterns = []; for (const file of dataFiles) { try { const content = await fs.readFile(file, 'utf8'); const dataStructures = this.extractDataStructures(content); patterns.push({ type: 'data_pattern', file: path.relative(repositoryPath, file), structures: dataStructures, consistency: this.calculateDataConsistency(dataStructures), coverage: this.calculateDataCoverage(dataStructures) }); } catch (error) { console.warn(`Failed to analyze data file ${file}:`, error.message); } } return patterns; } async generateRecommendations(scenario, existingPatterns) { const recommendations = []; // Analyze scenario for recommendations const scenarioText = scenario.gherkin_syntax.toLowerCase(); // Check for existing similar patterns const similarPatterns = this.findSimilarPatterns(scenario, existingPatterns); if (similarPatterns.length > 0) { recommendations.push({ type: 'reusability', priority: 'high', title: 'Reuse Existing Patterns', description: `Found ${similarPatterns.length} similar existing patterns that can be reused`, patterns: similarPatterns, savings: this.calculateTimeSavings(similarPatterns) }); } // Check for selector best practices const selectorRecommendations = this.generateSelectorRecommendations(scenario, existingPatterns); recommendations.push(...selectorRecommendations); // Check for architectural improvements const architecturalRecommendations = this.generateArchitecturalRecommendations(scenario, existingPatterns); recommendations.push(...architecturalRecommendations); // Check for performance optimizations const performanceRecommendations = this.generatePerformanceRecommendations(scenario, existingPatterns); recommendations.push(...performanceRecommendations); return recommendations.sort((a, b) => this.getPriorityWeight(b.priority) - this.getPriorityWeight(a.priority)); } generateSelectorRecommendations(scenario, existingPatterns) { const recommendations = []; const selectorPatterns = existingPatterns.filter(p => p.type === 'selector_pattern'); // Recommend most reliable selector patterns const reliablePatterns = selectorPatterns.filter(p => p.reliability > 0.8); if (reliablePatterns.length > 0) { recommendations.push({ type: 'selector_best_practice', priority: 'medium', title: 'Use Reliable Selector Patterns', description: 'Leverage proven selector patterns from your codebase', patterns: reliablePatterns.map(p => p.category), example: reliablePatterns[0].examples[0]?.selector }); } // Warn about brittle selectors const brittlePatterns = selectorPatterns.filter(p => p.reliability < 0.4); if (brittlePatterns.length > 0) { recommendations.push({ type: 'selector_warning', priority: 'high', title: 'Avoid Brittle Selector Patterns', description: 'Some selector patterns in your codebase are unreliable', patterns: brittlePatterns.map(p => p.category), suggestion: 'Consider using data-testid attributes or more stable selectors' }); } return recommendations; } generateArchitecturalRecommendations(scenario, existingPatterns) { const recommendations = []; const pagePatterns = existingPatterns.filter(p => p.type === 'page_object'); // Check for consistent page object structure if (pagePatterns.length > 0) { const structures = pagePatterns.map(p => this.analyzePageStructure(p)); const consistencyScore = this.calculateStructuralConsistency(structures); if (consistencyScore < 0.7) { recommendations.push({ type: 'architecture', priority: 'medium', title: 'Improve Page Object Consistency', description: 'Page objects show inconsistent structure patterns', suggestion: 'Establish and follow consistent page object patterns', current_score: consistencyScore }); } } // Check for inheritance opportunities const inheritanceOpportunities = this.findInheritanceOpportunities(pagePatterns); if (inheritanceOpportunities.length > 0) { recommendations.push({ type: 'architecture', priority: 'low', title: 'Consider Base Page Pattern', description: 'Common functionality could be extracted to base classes', opportunities: inheritanceOpportunities }); } return recommendations; } generatePerformanceRecommendations(scenario, existingPatterns) { const recommendations = []; // Check for potential performance issues const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition'); const complexSteps = stepPatterns.filter(p => p.complexity > 0.8); if (complexSteps.length > 0) { recommendations.push({ type: 'performance', priority: 'medium', title: 'Optimize Complex Steps', description: 'Some step definitions are overly complex', steps: complexSteps.map(s => s.file), suggestion: 'Break down complex steps into smaller, focused steps' }); } // Check for wait strategies const scenarioText = scenario.gherkin_syntax.toLowerCase(); if (scenarioText.includes('wait') || scenarioText.includes('load')) { recommendations.push({ type: 'performance', priority: 'high', title: 'Implement Smart Wait Strategies', description: 'Scenario involves waiting - use explicit waits over implicit delays', suggestion: 'Use WebDriverIO waitUntil() methods for better performance' }); } return recommendations; } calculateReusabilityScore(scenario, existingPatterns) { let score = 0; let maxScore = 100; // Check for reusable steps const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition'); const scenarioSteps = this.extractScenarioSteps(scenario.gherkin_syntax); let reusableSteps = 0; scenarioSteps.forEach(step => { stepPatterns.forEach(pattern => { if (pattern.patterns.some(p => this.calculateStepSimilarity(step, p) > 0.8)) { reusableSteps++; } }); }); score += (reusableSteps / Math.max(scenarioSteps.length, 1)) * 40; // Check for reusable selectors const selectorPatterns = existingPatterns.filter(p => p.type === 'selector_pattern'); if (selectorPatterns.length > 0) { score += 20; // Base score for having selector patterns } // Check for consistent patterns const consistency = this.calculatePatternConsistency(existingPatterns); score += consistency * 40; return Math.min(score, maxScore); } async generateFileSuggestions(scenario, existingPatterns) { const suggestions = { feature: await this.suggestFeatureFileStructure(scenario, existingPatterns), steps: await this.suggestStepsFileStructure(scenario, existingPatterns), page: await this.suggestPageFileStructure(scenario, existingPatterns), component: await this.suggestComponentFileStructure(scenario, existingPatterns) }; return suggestions; } async suggestFeatureFileStructure(scenario, existingPatterns) { const suggestion = { recommended_location: this.suggestFeatureLocation(scenario), naming_convention: this.suggestFeatureNaming(scenario), tags: this.suggestFeatureTags(scenario), structure: { include_background: this.shouldIncludeBackground(scenario), include_examples: this.shouldIncludeExamples(scenario), scenario_outline: this.shouldUseScenarioOutline(scenario) } }; return suggestion; } async suggestStepsFileStructure(scenario, existingPatterns) { const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition'); return { recommended_location: this.suggestStepsLocation(scenario), naming_convention: this.suggestStepsNaming(scenario), reusable_steps: this.findReusableSteps(scenario, stepPatterns), new_steps_needed: this.identifyNewStepsNeeded(scenario, stepPatterns), imports: this.suggestStepsImports(stepPatterns) }; } async suggestPageFileStructure(scenario, existingPatterns) { const pagePatterns = existingPatterns.filter(p => p.type === 'page_object'); return { recommended_location: this.suggestPageLocation(scenario), naming_convention: this.suggestPageNaming(scenario), inheritance: this.suggestPageInheritance(pagePatterns), methods: this.suggestPageMethods(scenario), selectors: this.suggestPageSelectors(scenario, existingPatterns) }; } async suggestComponentFileStructure(scenario, existingPatterns) { const dataPatterns = existingPatterns.filter(p => p.type === 'data_pattern'); return { recommended_location: this.suggestComponentLocation(scenario), naming_convention: this.suggestComponentNaming(scenario), data_structure: this.suggestDataStructure(scenario, dataPatterns), validation: this.suggestDataValidation(scenario), utilities: this.suggestDataUtilities(scenario) }; } generatePerformanceInsights(scenario, existingPatterns) { const insights = { estimated_execution_time: this.estimateExecutionTime(scenario), complexity_metrics: this.calculateComplexityMetrics(scenario), bottleneck_analysis: this.identifyPotentialBottlenecks(scenario), optimization_opportunities: this.findOptimizationOpportunities(scenario, existingPatterns), resource_usage: this.estimateResourceUsage(scenario) }; return insights; } // Utility methods for analysis calculateComplexity(scenario) { const steps = this.extractScenarioSteps(scenario.gherkin_syntax); const uniqueActions = new Set(); const interactions = []; steps.forEach(step => { const action = this.extractActionType(step); uniqueActions.add(action); if (this.isUserInteraction(step)) { interactions.push(action); } }); const complexity = { step_count: steps.length, unique_actions: uniqueActions.size, interaction_count: interactions.length, has_loops: this.hasLoops(scenario.gherkin_syntax), has_conditions: this.hasConditions(scenario.gherkin_syntax), score: this.calculateComplexityScore(steps.length, uniqueActions.size, interactions.length) }; return complexity; } identifyDomain(scenario) { const text = (scenario.scenario_title + ' ' + scenario.gherkin_syntax).toLowerCase(); const domains = { 'ecommerce': ['buy', 'cart', 'checkout', 'payment', 'order', 'product'], 'authentication': ['login', 'register', 'password', 'user', 'account'], 'admin': ['admin', 'manage', 'configure', 'settings', 'dashboard'], 'api': ['api', 'endpoint', 'request', 'response', 'json'], 'form': ['form', 'input', 'submit', 'field', 'validation'], 'navigation': ['menu', 'navigate', 'page', 'link', 'redirect'] }; for (const [domain, keywords] of Object.entries(domains)) { if (keywords.some(keyword => text.includes(keyword))) { return domain; } } return 'general'; } classifyTestType(scenario) { const text = scenario.gherkin_syntax.toLowerCase(); if (text.includes('api') || text.includes('endpoint')) return 'api'; if (text.includes('ui') || text.includes('browser') || text.includes('click')) return 'ui'; if (text.includes('mobile') || text.includes('app')) return 'mobile'; if (text.includes('performance') || text.includes('load')) return 'performance'; if (text.includes('security') || text.includes('auth')) return 'security'; return 'functional'; } extractStepDefinitions(content) { const stepRegex = /(Given|When|Then|And|But)\s*\(\s*['"](.*?)['"]/g; const definitions = []; let match; while ((match = stepRegex.exec(content)) !== null) { definitions.push({ type: match[1], pattern: match[2], full_match: match[0] }); } return definitions; } analyzePageObject(content) { const analysis = { selectors: this.extractPageSelectors(content), methods: this.extractPageMethods(content), inheritance: this.checkInheritance(content), best_practices: this.checkPageObjectBestPractices(content) }; return analysis; } extractSelectors(content) { const selectorPatterns = [ /\$\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g, /\$\$\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g, /querySelector\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g, /getElementById\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g ]; const selectors = []; selectorPatterns.forEach(pattern => { let match; while ((match = pattern.exec(content)) !== null) { selectors.push({ value: match[1], type: this.determineSelectorType(match[1]), line: content.substring(0, match.index).split('\n').length }); } }); return selectors; } categorizeSelector(selector) { const value = selector.value; let category = 'unknown'; let confidence = 0.5; if (value.startsWith('[data-testid')) { category = 'data-testid'; confidence = 0.9; } else if (value.startsWith('#')) { category = 'id'; confidence = 0.8; } else if (value.startsWith('.')) { category = 'class'; confidence = 0.6; } else if (value.includes('[') && value.includes(']')) { category = 'attribute'; confidence = 0.7; } else if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) { category = 'tag'; confidence = 0.4; } else { category = 'complex'; confidence = 0.3; } return { category, confidence }; } extractDataStructures(content) { const exportRegex = /export\s+(?:const|let|var)\s+(\w+)\s*=\s*({[\s\S]*?});?/g; const structures = []; let match; while ((match = exportRegex.exec(content)) !== null) { try { const name = match[1]; const objectContent = match[2]; const structure = this.parseObjectStructure(objectContent); structures.push({ name, structure }); } catch (error) { // Skip invalid structures } } return structures; } findSimilarPatterns(scenario, existingPatterns) { const similar = []; const scenarioSteps = this.extractScenarioSteps(scenario.gherkin_syntax); existingPatterns.forEach(pattern => { if (pattern.type === 'step_definition') { pattern.patterns.forEach(stepPattern => { scenarioSteps.forEach(scenarioStep => { const similarity = this.calculateStepSimilarity(scenarioStep, stepPattern.pattern); if (similarity > this.config.getStepSimilarityThreshold()) { similar.push({ type: 'step', pattern: stepPattern, similarity, scenario_step: scenarioStep, file: pattern.file }); } }); }); } }); return similar; } calculateStepSimilarity(step1, step2) { const normalize = (str) => str.toLowerCase().replace(/['"]/g, '').replace(/\s+/g, ' ').trim(); const norm1 = normalize(step1); const norm2 = normalize(step2); if (norm1 === norm2) return 1.0; const words1 = norm1.split(' '); const words2 = norm2.split(' '); const intersection = words1.filter(word => words2.includes(word)); const union = [...new Set([...words1, ...words2])]; return intersection.length / union.length; } extractScenarioSteps(gherkinSyntax) { const stepRegex = /(Given|When|Then|And|But)\s+(.+)/g; const steps = []; let match; while ((match = stepRegex.exec(gherkinSyntax)) !== null) { steps.push(match[2].trim()); } return steps; } // Helper methods for various calculations and utilities calculateStepReusability(stepDefinitions) { // Calculate based on genericity of step patterns let score = 0; stepDefinitions.forEach(step => { if (step.pattern.includes('.*') || step.pattern.includes('\\w+')) { score += 0.8; } else if (step.pattern.length < 50) { score += 0.6; } else { score += 0.3; } }); return score / Math.max(stepDefinitions.length, 1); } calculateStepComplexity(stepDefinitions) { let totalComplexity = 0; stepDefinitions.forEach(step => { const patternComplexity = (step.pattern.match(/[.*+?^${}()|[\]\\]/g) || []).length; totalComplexity += Math.min(patternComplexity / 10, 1); }); return totalComplexity / Math.max(stepDefinitions.length, 1); } calculateSelectorReliability(selectors) { let totalReliability = 0; selectors.forEach(selector => { totalReliability += selector.confidence || 0.5; }); return totalReliability / Math.max(selectors.length, 1); } calculateDataConsistency(dataStructures) { if (dataStructures.length < 2) return 1.0; const fieldSets = dataStructures.map(ds => new Set(Object.keys(ds.structure || {}))); const allFields = new Set(); fieldSets.forEach(fields => fields.forEach(field => allFields.add(field))); let consistencyScore = 0; for (let i = 0; i < fieldSets.length - 1; i++) { for (let j = i + 1; j < fieldSets.length; j++) { const intersection = new Set([...fieldSets[i]].filter(x => fieldSets[j].has(x))); const union = new Set([...fieldSets[i], ...fieldSets[j]]); consistencyScore += intersection.size / union.size; } } return consistencyScore / ((fieldSets.length * (fieldSets.length - 1)) / 2); } calculateDataCoverage(dataStructures) { const allFields = new Set(); const coveredFields = new Set(); dataStructures.forEach(ds => { const structure = ds.structure || {}; Object.keys(structure).forEach(field => { allFields.add(field); if (structure[field] !== null && structure[field] !== undefined) { coveredFields.add(field); } }); }); return allFields.size > 0 ? coveredFields.size / allFields.size : 1.0; } generateCacheKey(repositoryPath, scenario) { const keyData = { path: repositoryPath, title: scenario.scenario_title, gherkin: scenario.gherkin_syntax.substring(0, 100) // First 100 chars for uniqueness }; return Buffer.from(JSON.stringify(keyData)).toString('base64'); } getFallbackAnalysis(scenario) { return { timestamp: new Date().toISOString(), scenario_analysis: { title: scenario.scenario_title, complexity: { score: 0.5 }, domain: 'general', test_type: 'functional' }, patterns: [], recommendations: [{ type: 'fallback', priority: 'low', title: 'Basic Analysis Only', description: 'Full smart analysis unavailable, using basic patterns' }], file_suggestions: {}, reusability_score: 0, performance_insights: {} }; } getPriorityWeight(priority) { const weights = { high: 3, medium: 2, low: 1 }; return weights[priority] || 1; } // Additional utility methods would be implemented here // Due to space constraints, showing key structure and main methods clearCache() { this.cache.clear(); console.log('Smart analysis cache cleared'); } getAnalysisHistory() { return this.analysisHistory; } exportAnalysis(analysis, format = 'json') { if (format === 'json') { return JSON.stringify(analysis, null, 2); } // Add other export formats as needed return analysis; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/raymondsambur/automation-script-generator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server