Skip to main content
Glama
ooples

MCP Console Automation Server

FlakeDetector.ts9.48 kB
/** * FlakeDetector - Detects flaky tests by running them multiple times */ import { TestDefinition, TestResult, FlakeReport, } from '../types/test-framework.js'; export interface FlakeDetectionConfig { runs: number; threshold: number; // Flake rate threshold (0-1) parallel: boolean; stopOnStable?: boolean; // Stop if test is consistently passing/failing } export class FlakeDetector { private defaultConfig: FlakeDetectionConfig = { runs: 10, threshold: 0.1, // 10% flake rate parallel: true, stopOnStable: false, }; /** * Detect if a test is flaky */ async detectFlake( test: TestDefinition, executor: (test: TestDefinition) => Promise<TestResult>, config?: Partial<FlakeDetectionConfig> ): Promise<FlakeReport> { const detectionConfig = { ...this.defaultConfig, ...config }; const results: TestResult[] = []; // Run test multiple times if (detectionConfig.parallel) { // Run in parallel const promises = Array.from({ length: detectionConfig.runs }, () => executor(test) ); results.push(...(await Promise.all(promises))); } else { // Run sequentially for (let i = 0; i < detectionConfig.runs; i++) { const result = await executor(test); results.push(result); // Early exit if test is stable if (detectionConfig.stopOnStable && i >= 4) { const stable = this.isStable(results); if (stable) { break; } } } } // Analyze results return this.analyzeResults(test.name, results, detectionConfig.threshold); } /** * Detect flaky tests in a batch */ async detectFlakyTests( tests: TestDefinition[], executor: (test: TestDefinition) => Promise<TestResult>, config?: Partial<FlakeDetectionConfig> ): Promise<FlakeReport[]> { const reports: FlakeReport[] = []; for (const test of tests) { const report = await this.detectFlake(test, executor, config); if (report.isFlaky) { reports.push(report); } } return reports; } /** * Analyze test results and generate flake report */ private analyzeResults( testName: string, results: TestResult[], threshold: number ): FlakeReport { const totalRuns = results.length; const passes = results.filter((r) => r.status === 'pass').length; const failures = results.filter((r) => r.status === 'fail').length; const errors = results.filter( (r) => r.status === 'timeout' || r.error !== undefined ).length; // Calculate flake rate const flakeRate = this.calculateFlakeRate(results); // Extract failure patterns const failurePatterns = this.extractFailurePatterns(results); return { testName, totalRuns, passes, failures, errors, flakeRate, isFlaky: flakeRate > threshold, threshold, failurePatterns, }; } /** * Calculate flake rate */ private calculateFlakeRate(results: TestResult[]): number { if (results.length === 0) return 0; const passes = results.filter((r) => r.status === 'pass').length; const failures = results.length - passes; // Flake rate is the percentage of runs that differ from the majority const minority = Math.min(passes, failures); return minority / results.length; } /** * Check if test results are stable (all same outcome) */ private isStable(results: TestResult[]): boolean { if (results.length < 3) return false; const firstStatus = results[0].status; return results.every((r) => r.status === firstStatus); } /** * Extract common failure patterns */ private extractFailurePatterns(results: TestResult[]): string[] { const patterns = new Map<string, number>(); for (const result of results) { if (result.status === 'fail' && result.error) { const pattern = this.categorizeError(result.error); patterns.set(pattern, (patterns.get(pattern) || 0) + 1); } } // Return patterns sorted by frequency return Array.from(patterns.entries()) .sort((a, b) => b[1] - a[1]) .map(([pattern, count]) => `${pattern} (${count} times)`); } /** * Categorize error into pattern */ private categorizeError(error: Error): string { const message = error.message.toLowerCase(); if (message.includes('timeout')) return 'Timeout'; if (message.includes('connection')) return 'Connection Error'; if (message.includes('network')) return 'Network Error'; if (message.includes('assertion')) return 'Assertion Failure'; if (message.includes('undefined') || message.includes('null')) return 'Null/Undefined Error'; if (message.includes('race') || message.includes('concurrent')) return 'Race Condition'; return 'Unknown Error'; } /** * Generate flake summary for multiple tests */ generateFlakeSummary(reports: FlakeReport[]) { const totalTests = reports.length; const flakyTests = reports.filter((r) => r.isFlaky).length; const stableTests = totalTests - flakyTests; // Calculate average flake rate const averageFlakeRate = reports.reduce((sum, r) => sum + r.flakeRate, 0) / totalTests || 0; // Group by flake rate severity const severe = reports.filter((r) => r.flakeRate > 0.3).length; // >30% flake const moderate = reports.filter( (r) => r.flakeRate > 0.1 && r.flakeRate <= 0.3 ).length; // 10-30% const mild = reports.filter( (r) => r.flakeRate > 0 && r.flakeRate <= 0.1 ).length; // <10% return { totalTests, flakyTests, stableTests, flakeRate: flakyTests / totalTests || 0, averageFlakeRate, severity: { severe, moderate, mild, }, mostFlakyTest: this.getMostFlakyTest(reports), commonPatterns: this.getCommonPatterns(reports), }; } /** * Get the most flaky test */ private getMostFlakyTest(reports: FlakeReport[]): string | null { if (reports.length === 0) return null; const sorted = [...reports].sort((a, b) => b.flakeRate - a.flakeRate); return sorted[0].testName; } /** * Get common failure patterns across all tests */ private getCommonPatterns(reports: FlakeReport[]): string[] { const allPatterns = new Map<string, number>(); for (const report of reports) { if (report.failurePatterns) { for (const pattern of report.failurePatterns) { // Extract pattern name (before the count) const patternName = pattern.split(' (')[0]; allPatterns.set(patternName, (allPatterns.get(patternName) || 0) + 1); } } } return Array.from(allPatterns.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5) // Top 5 .map(([pattern, count]) => `${pattern} (${count} tests)`); } /** * Generate recommendations based on flake analysis */ generateRecommendations(report: FlakeReport): string[] { const recommendations: string[] = []; if (report.flakeRate > 0.5) { recommendations.push( 'Test is highly unstable - consider major refactoring or removal' ); } else if (report.flakeRate > 0.3) { recommendations.push( 'Test shows significant instability - investigate root cause' ); } // Check failure patterns if (report.failurePatterns) { for (const pattern of report.failurePatterns) { if (pattern.includes('Timeout')) { recommendations.push('Add explicit waits or increase timeout values'); } if (pattern.includes('Race Condition')) { recommendations.push( 'Add synchronization or use proper locking mechanisms' ); } if (pattern.includes('Network Error')) { recommendations.push('Add network retry logic or use mocking'); } if (pattern.includes('Connection Error')) { recommendations.push('Ensure proper connection pooling and cleanup'); } } } if (recommendations.length === 0) { recommendations.push('Monitor test over time to identify patterns'); } return recommendations; } } /** * Flake detector with statistical analysis */ export class StatisticalFlakeDetector extends FlakeDetector { /** * Calculate statistical confidence in flake detection */ calculateConfidence(report: FlakeReport): { confidence: number; sampleSizeAdequate: boolean; } { const sampleSize = report.totalRuns; const flakeRate = report.flakeRate; // Simple confidence calculation based on sample size // More runs = higher confidence const confidence = Math.min(sampleSize / 20, 1); // Max confidence at 20 runs // Check if sample size is adequate for the flake rate // Need more runs for low flake rates const minSampleSize = flakeRate < 0.1 ? 20 : 10; const sampleSizeAdequate = sampleSize >= minSampleSize; return { confidence, sampleSizeAdequate, }; } /** * Recommend optimal number of runs for a test */ recommendRuns(previousReport?: FlakeReport): number { if (!previousReport) { return 10; // Default } // More flaky tests need more runs if (previousReport.flakeRate > 0.3) { return 20; } else if (previousReport.flakeRate > 0.1) { return 15; } else { return 10; } } }

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/ooples/mcp-console-automation'

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