Skip to main content
Glama
by Coder-RL
test-orchestrator.ts43.9 kB
import { EventEmitter } from 'events'; export interface TestSuite { id: string; name: string; description?: string; type: 'unit' | 'integration' | 'e2e' | 'performance' | 'security' | 'accessibility' | 'api' | 'load'; framework: 'jest' | 'mocha' | 'cypress' | 'playwright' | 'k6' | 'postman' | 'custom'; configuration: TestConfiguration; tests: TestCase[]; setup: TestSetup; teardown: TestTeardown; environment: TestEnvironment; dependencies: string[]; tags: string[]; owner: string; schedule?: TestSchedule; retryPolicy: RetryPolicy; timeout: number; parallel: boolean; coverage: CoverageConfig; createdAt: Date; updatedAt: Date; status: 'active' | 'inactive' | 'deprecated'; } export interface TestConfiguration { baseUrl?: string; browser?: string; viewport?: { width: number; height: number }; userAgent?: string; authentication?: AuthConfig; database?: DatabaseConfig; mocks?: MockConfig[]; fixtures?: FixtureConfig[]; environment: Record<string, string>; customSettings: Record<string, any>; } export interface AuthConfig { type: 'none' | 'basic' | 'bearer' | 'oauth2' | 'custom'; credentials: Record<string, string>; tokenEndpoint?: string; refreshToken?: boolean; } export interface DatabaseConfig { type: string; connectionString: string; resetBetweenTests: boolean; seedData?: string[]; migrations?: string[]; } export interface MockConfig { service: string; endpoint: string; method: string; response: any; statusCode: number; delay?: number; conditions?: Record<string, any>; } export interface FixtureConfig { name: string; type: 'json' | 'csv' | 'sql' | 'custom'; source: string; preprocess?: string; } export interface TestCase { id: string; name: string; description?: string; category: string; priority: 'low' | 'medium' | 'high' | 'critical'; type: 'manual' | 'automated'; steps: TestStep[]; assertions: Assertion[]; prerequisites: string[]; testData: TestData[]; expectedResults: ExpectedResult[]; tags: string[]; owner: string; automated: boolean; flaky: boolean; timeout: number; retries: number; createdAt: Date; updatedAt: Date; lastRun?: Date; status: 'active' | 'inactive' | 'skipped'; } export interface TestStep { id: string; description: string; action: TestAction; parameters: Record<string, any>; expected?: string; screenshot?: boolean; wait?: number; condition?: string; } export interface TestAction { type: 'navigate' | 'click' | 'type' | 'select' | 'wait' | 'scroll' | 'api_call' | 'database_query' | 'custom'; target?: string; value?: any; method?: string; headers?: Record<string, string>; body?: any; query?: string; script?: string; } export interface Assertion { id: string; type: 'equals' | 'contains' | 'exists' | 'visible' | 'enabled' | 'count' | 'regex' | 'custom'; target: string; expected: any; actual?: any; operator?: string; message?: string; severity: 'error' | 'warning'; } export interface TestData { name: string; type: 'static' | 'dynamic' | 'generated' | 'fixture'; value: any; source?: string; generator?: string; } export interface ExpectedResult { description: string; criteria: string; type: 'functional' | 'performance' | 'security' | 'accessibility'; metric?: string; threshold?: number; } export interface TestSetup { scripts: string[]; database?: DatabaseSetup; services?: ServiceSetup[]; environment?: Record<string, string>; fixtures?: string[]; } export interface DatabaseSetup { reset: boolean; migrate: boolean; seed: boolean; scripts: string[]; } export interface ServiceSetup { name: string; command: string; healthCheck: string; timeout: number; environment?: Record<string, string>; } export interface TestTeardown { scripts: string[]; cleanup: boolean; resetDatabase: boolean; stopServices: boolean; archiveLogs: boolean; } export interface TestEnvironment { name: string; type: 'local' | 'docker' | 'kubernetes' | 'cloud'; configuration: Record<string, any>; resources: ResourceRequirements; isolation: boolean; } export interface ResourceRequirements { cpu: string; memory: string; storage: string; network: boolean; } export interface TestSchedule { enabled: boolean; cron: string; timezone: string; branches: string[]; triggers: TestTrigger[]; notifications: string[]; } export interface TestTrigger { type: 'push' | 'pull_request' | 'schedule' | 'manual' | 'deployment' | 'webhook'; conditions: TriggerCondition[]; branches?: string[]; files?: string[]; } export interface TriggerCondition { field: string; operator: 'equals' | 'contains' | 'regex' | 'exists'; value: any; } export interface RetryPolicy { maxRetries: number; retryOn: string[]; backoffStrategy: 'fixed' | 'exponential' | 'linear'; initialDelay: number; maxDelay: number; } export interface CoverageConfig { enabled: boolean; threshold: number; includePatterns: string[]; excludePatterns: string[]; reportFormats: string[]; enforceThreshold: boolean; } export interface TestExecution { id: string; suiteId: string; name: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timeout'; trigger: string; triggeredBy: string; environment: string; startTime: Date; endTime?: Date; duration?: number; results: TestResult[]; summary: TestSummary; coverage?: CoverageReport; artifacts: TestArtifact[]; logs: TestLog[]; metrics: TestMetrics; parallelExecutions: number; retryCount: number; } export interface TestResult { testId: string; name: string; status: 'passed' | 'failed' | 'skipped' | 'error'; startTime: Date; endTime: Date; duration: number; steps: StepResult[]; assertions: AssertionResult[]; error?: TestError; screenshots: string[]; logs: string[]; metrics: StepMetrics; retries: number; flaky: boolean; } export interface StepResult { stepId: string; description: string; status: 'passed' | 'failed' | 'skipped'; startTime: Date; endTime: Date; duration: number; screenshot?: string; error?: string; output?: any; } export interface AssertionResult { assertionId: string; status: 'passed' | 'failed'; expected: any; actual: any; message?: string; diff?: string; } export interface TestError { message: string; stack?: string; type: string; code?: string; line?: number; column?: number; screenshot?: string; } export interface StepMetrics { responseTime?: number; memoryUsage?: number; cpuUsage?: number; networkRequests?: number; customMetrics?: Record<string, number>; } export interface TestSummary { total: number; passed: number; failed: number; skipped: number; error: number; passRate: number; duration: number; coverage?: number; flaky: number; } export interface CoverageReport { overall: number; byFile: Record<string, FileCoverage>; byFunction: Record<string, FunctionCoverage>; byLine: Record<string, LineCoverage>; thresholdMet: boolean; reportUrl?: string; } export interface FileCoverage { path: string; coverage: number; lines: { covered: number; total: number }; functions: { covered: number; total: number }; branches: { covered: number; total: number }; } export interface FunctionCoverage { name: string; file: string; line: number; covered: boolean; hits: number; } export interface LineCoverage { file: string; line: number; covered: boolean; hits: number; } export interface TestArtifact { id: string; type: 'screenshot' | 'video' | 'log' | 'report' | 'coverage' | 'performance'; name: string; path: string; size: number; mimeType: string; metadata: Record<string, any>; createdAt: Date; } export interface TestLog { timestamp: Date; level: 'debug' | 'info' | 'warn' | 'error'; source: string; message: string; metadata?: Record<string, any>; } export interface TestMetrics { executionTime: number; setupTime: number; teardownTime: number; resourceUsage: ResourceUsage; errorRate: number; flakyRate: number; customMetrics: Record<string, number>; } export interface ResourceUsage { cpu: { max: number; avg: number }; memory: { max: number; avg: number }; disk: { read: number; write: number }; network: { in: number; out: number }; } export interface QualityGate { id: string; name: string; description: string; conditions: QualityCondition[]; enforcement: 'blocking' | 'warning' | 'informational'; applicableTypes: string[]; environment: string[]; createdBy: string; createdAt: Date; updatedAt: Date; active: boolean; } export interface QualityCondition { metric: string; operator: 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'ne'; threshold: number; severity: 'blocker' | 'critical' | 'major' | 'minor' | 'info'; errorMessage: string; } export interface TestPlan { id: string; name: string; description: string; version: string; suites: string[]; environment: string; schedule: TestSchedule; qualityGates: string[]; notifications: NotificationConfig; approvals: ApprovalConfig; rollback: RollbackConfig; owner: string; createdAt: Date; updatedAt: Date; status: 'active' | 'inactive' | 'archived'; } export interface NotificationConfig { channels: NotificationChannel[]; events: string[]; conditions: NotificationCondition[]; } export interface NotificationChannel { type: 'email' | 'slack' | 'teams' | 'webhook'; config: Record<string, any>; recipients: string[]; } export interface NotificationCondition { event: string; condition: string; severity: string[]; } export interface ApprovalConfig { required: boolean; approvers: string[]; conditions: string[]; timeout: number; } export interface RollbackConfig { enabled: boolean; conditions: string[]; strategy: 'automatic' | 'manual'; timeout: number; } export interface TestReport { id: string; executionId: string; type: 'summary' | 'detailed' | 'coverage' | 'performance' | 'trend'; format: 'html' | 'pdf' | 'json' | 'xml' | 'junit'; content: string; metadata: ReportMetadata; generatedAt: Date; expiresAt?: Date; } export interface ReportMetadata { version: string; generator: string; filters: Record<string, any>; aggregation: string; timeRange?: { start: Date; end: Date }; } export class TestOrchestrator extends EventEmitter { private testSuites = new Map<string, TestSuite>(); private executions = new Map<string, TestExecution>(); private qualityGates = new Map<string, QualityGate>(); private testPlans = new Map<string, TestPlan>(); private activeExecutions = new Map<string, TestExecution>(); private testRunners = new Map<string, TestRunner>(); private reports = new Map<string, TestReport>(); constructor() { super(); this.initializeTestRunners(); this.startScheduler(); this.startMetricsCollection(); } // Test Suite Management async createTestSuite(suite: Omit<TestSuite, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> { try { const suiteId = this.generateId(); const testSuite: TestSuite = { id: suiteId, createdAt: new Date(), updatedAt: new Date(), ...suite }; // Validate test suite await this.validateTestSuite(testSuite); this.testSuites.set(suiteId, testSuite); this.emit('testSuiteCreated', { suite: testSuite }); return suiteId; } catch (error) { this.emit('error', { operation: 'createTestSuite', error }); throw error; } } async updateTestSuite(suiteId: string, updates: Partial<TestSuite>): Promise<boolean> { try { const suite = this.testSuites.get(suiteId); if (!suite) { throw new Error(`Test suite ${suiteId} not found`); } Object.assign(suite, updates, { updatedAt: new Date() }); if (updates.tests || updates.configuration) { await this.validateTestSuite(suite); } this.emit('testSuiteUpdated', { suite }); return true; } catch (error) { this.emit('error', { operation: 'updateTestSuite', error }); return false; } } async deleteTestSuite(suiteId: string): Promise<boolean> { try { const suite = this.testSuites.get(suiteId); if (!suite) { return false; } // Check for active executions const activeExecution = Array.from(this.activeExecutions.values()) .find(e => e.suiteId === suiteId); if (activeExecution) { throw new Error('Cannot delete test suite with active execution'); } this.testSuites.delete(suiteId); this.emit('testSuiteDeleted', { suiteId }); return true; } catch (error) { this.emit('error', { operation: 'deleteTestSuite', error }); return false; } } async getTestSuite(suiteId: string): Promise<TestSuite | undefined> { return this.testSuites.get(suiteId); } async getTestSuites(filters?: { type?: string; framework?: string; owner?: string; tags?: string[]; }): Promise<TestSuite[]> { let suites = Array.from(this.testSuites.values()); if (filters) { if (filters.type) { suites = suites.filter(s => s.type === filters.type); } if (filters.framework) { suites = suites.filter(s => s.framework === filters.framework); } if (filters.owner) { suites = suites.filter(s => s.owner === filters.owner); } if (filters.tags && filters.tags.length > 0) { suites = suites.filter(s => filters.tags!.some(tag => s.tags.includes(tag)) ); } } return suites.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); } // Test Execution async executeTestSuite( suiteId: string, options: { environment?: string; parallel?: boolean; tags?: string[]; testIds?: string[]; triggeredBy?: string; trigger?: string; } = {} ): Promise<string> { try { const suite = this.testSuites.get(suiteId); if (!suite) { throw new Error(`Test suite ${suiteId} not found`); } const executionId = this.generateId(); const execution: TestExecution = { id: executionId, suiteId, name: `${suite.name} - ${new Date().toISOString()}`, status: 'pending', trigger: options.trigger || 'manual', triggeredBy: options.triggeredBy || 'system', environment: options.environment || 'default', startTime: new Date(), results: [], summary: { total: 0, passed: 0, failed: 0, skipped: 0, error: 0, passRate: 0, duration: 0, flaky: 0 }, artifacts: [], logs: [], metrics: { executionTime: 0, setupTime: 0, teardownTime: 0, resourceUsage: { cpu: { max: 0, avg: 0 }, memory: { max: 0, avg: 0 }, disk: { read: 0, write: 0 }, network: { in: 0, out: 0 } }, errorRate: 0, flakyRate: 0, customMetrics: {} }, parallelExecutions: options.parallel ? 4 : 1, retryCount: 0 }; this.executions.set(executionId, execution); this.activeExecutions.set(executionId, execution); this.emit('executionStarted', { execution }); // Start execution asynchronously this.processExecution(execution, suite, options); return executionId; } catch (error) { this.emit('error', { operation: 'executeTestSuite', error }); throw error; } } async stopExecution(executionId: string): Promise<boolean> { try { const execution = this.activeExecutions.get(executionId); if (!execution || execution.status !== 'running') { return false; } execution.status = 'cancelled'; execution.endTime = new Date(); execution.duration = execution.endTime.getTime() - execution.startTime.getTime(); this.activeExecutions.delete(executionId); this.emit('executionStopped', { execution }); return true; } catch (error) { this.emit('error', { operation: 'stopExecution', error }); return false; } } async getExecution(executionId: string): Promise<TestExecution | undefined> { return this.executions.get(executionId); } async getExecutions( suiteId?: string, limit: number = 50 ): Promise<TestExecution[]> { let executions = Array.from(this.executions.values()); if (suiteId) { executions = executions.filter(e => e.suiteId === suiteId); } return executions .sort((a, b) => b.startTime.getTime() - a.startTime.getTime()) .slice(0, limit); } async getActiveExecutions(): Promise<TestExecution[]> { return Array.from(this.activeExecutions.values()); } // Quality Gates async createQualityGate(gate: Omit<QualityGate, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> { try { const gateId = this.generateId(); const qualityGate: QualityGate = { id: gateId, createdAt: new Date(), updatedAt: new Date(), ...gate }; this.qualityGates.set(gateId, qualityGate); this.emit('qualityGateCreated', { gate: qualityGate }); return gateId; } catch (error) { this.emit('error', { operation: 'createQualityGate', error }); throw error; } } async evaluateQualityGates( executionId: string, gateIds: string[] ): Promise<{ passed: boolean; results: QualityGateResult[] }> { try { const execution = this.executions.get(executionId); if (!execution) { throw new Error(`Execution ${executionId} not found`); } const results: QualityGateResult[] = []; let overallPassed = true; for (const gateId of gateIds) { const gate = this.qualityGates.get(gateId); if (!gate || !gate.active) continue; const result = await this.evaluateQualityGate(gate, execution); results.push(result); if (!result.passed && gate.enforcement === 'blocking') { overallPassed = false; } } this.emit('qualityGatesEvaluated', { executionId, results, passed: overallPassed }); return { passed: overallPassed, results }; } catch (error) { this.emit('error', { operation: 'evaluateQualityGates', error }); throw error; } } // Test Plans async createTestPlan(plan: Omit<TestPlan, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> { try { const planId = this.generateId(); const testPlan: TestPlan = { id: planId, createdAt: new Date(), updatedAt: new Date(), ...plan }; this.testPlans.set(planId, testPlan); this.emit('testPlanCreated', { plan: testPlan }); return planId; } catch (error) { this.emit('error', { operation: 'createTestPlan', error }); throw error; } } async executeTestPlan(planId: string, triggeredBy: string): Promise<string[]> { try { const plan = this.testPlans.get(planId); if (!plan) { throw new Error(`Test plan ${planId} not found`); } const executionIds: string[] = []; for (const suiteId of plan.suites) { const executionId = await this.executeTestSuite(suiteId, { environment: plan.environment, triggeredBy, trigger: 'test_plan' }); executionIds.push(executionId); } this.emit('testPlanExecuted', { planId, executionIds }); return executionIds; } catch (error) { this.emit('error', { operation: 'executeTestPlan', error }); throw error; } } // Reporting async generateReport( executionId: string, type: 'summary' | 'detailed' | 'coverage' | 'performance' | 'trend', format: 'html' | 'pdf' | 'json' | 'xml' | 'junit' ): Promise<string> { try { const execution = this.executions.get(executionId); if (!execution) { throw new Error(`Execution ${executionId} not found`); } const reportId = this.generateId(); const content = await this.generateReportContent(execution, type, format); const report: TestReport = { id: reportId, executionId, type, format, content, metadata: { version: '1.0.0', generator: 'Claude MCP Test Orchestrator', filters: {}, aggregation: 'execution' }, generatedAt: new Date() }; this.reports.set(reportId, report); this.emit('reportGenerated', { report }); return reportId; } catch (error) { this.emit('error', { operation: 'generateReport', error }); throw error; } } async getReport(reportId: string): Promise<TestReport | undefined> { return this.reports.get(reportId); } // Analytics async getTestAnalytics( timeRange?: { start: Date; end: Date }, filters?: { suiteId?: string; environment?: string; type?: string } ): Promise<{ executions: { total: number; passed: number; failed: number; passRate: number }; performance: { avgDuration: number; trend: 'improving' | 'degrading' | 'stable' }; quality: { coverageAvg: number; flakyTests: number; topFailures: string[] }; efficiency: { automationRate: number; maintenanceCost: number }; }> { const executions = Array.from(this.executions.values()).filter(e => { if (timeRange && (e.startTime < timeRange.start || e.startTime > timeRange.end)) { return false; } if (filters?.suiteId && e.suiteId !== filters.suiteId) { return false; } if (filters?.environment && e.environment !== filters.environment) { return false; } return true; }); const total = executions.length; const passed = executions.filter(e => e.status === 'completed' && e.summary.failed === 0).length; const failed = executions.filter(e => e.status === 'failed' || e.summary.failed > 0).length; const passRate = total > 0 ? passed / total : 0; const avgDuration = executions.length > 0 ? executions.reduce((sum, e) => sum + (e.duration || 0), 0) / executions.length : 0; const coverageAvg = executions .filter(e => e.coverage) .reduce((sum, e) => sum + (e.coverage!.overall || 0), 0) / Math.max(executions.length, 1); const flakyTests = executions.reduce((sum, e) => sum + e.summary.flaky, 0); // Calculate automation rate const allTests = Array.from(this.testSuites.values()) .flatMap(s => s.tests); const automatedTests = allTests.filter(t => t.automated); const automationRate = allTests.length > 0 ? automatedTests.length / allTests.length : 0; return { executions: { total, passed, failed, passRate }, performance: { avgDuration, trend: 'stable' }, quality: { coverageAvg, flakyTests, topFailures: [] }, efficiency: { automationRate, maintenanceCost: 0 } }; } async getTestTrends( period: 'daily' | 'weekly' | 'monthly', metric: 'pass_rate' | 'duration' | 'coverage' | 'flakiness' ): Promise<{ date: Date; value: number }[]> { // Generate mock trend data const trends: { date: Date; value: number }[] = []; const periodDays = period === 'daily' ? 1 : period === 'weekly' ? 7 : 30; const dataPoints = 30; // Last 30 data points for (let i = dataPoints - 1; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - (i * periodDays)); let value: number; switch (metric) { case 'pass_rate': value = 0.85 + Math.random() * 0.1; // 85-95% break; case 'duration': value = 300 + Math.random() * 100; // 300-400 seconds break; case 'coverage': value = 0.75 + Math.random() * 0.15; // 75-90% break; case 'flakiness': value = Math.random() * 0.05; // 0-5% break; default: value = Math.random(); } trends.push({ date, value }); } return trends; } private async processExecution( execution: TestExecution, suite: TestSuite, options: any ): Promise<void> { try { execution.status = 'running'; this.emit('executionRunning', { execution }); // Setup phase const setupStart = Date.now(); await this.setupTestEnvironment(suite, execution); execution.metrics.setupTime = Date.now() - setupStart; // Filter tests based on options let testsToRun = suite.tests.filter(t => t.status === 'active'); if (options.tags && options.tags.length > 0) { testsToRun = testsToRun.filter(t => options.tags.some((tag: string) => t.tags.includes(tag)) ); } if (options.testIds && options.testIds.length > 0) { testsToRun = testsToRun.filter(t => options.testIds.includes(t.id)); } execution.summary.total = testsToRun.length; // Execute tests const testRunner = this.testRunners.get(suite.framework); if (!testRunner) { throw new Error(`Test runner for ${suite.framework} not found`); } if (options.parallel && execution.parallelExecutions > 1) { await this.executeTestsInParallel(testsToRun, testRunner, execution, suite); } else { await this.executeTestsSequentially(testsToRun, testRunner, execution, suite); } // Calculate summary this.calculateExecutionSummary(execution); // Generate coverage report if enabled if (suite.coverage.enabled) { execution.coverage = await this.generateCoverageReport(execution, suite); } // Teardown phase const teardownStart = Date.now(); await this.teardownTestEnvironment(suite, execution); execution.metrics.teardownTime = Date.now() - teardownStart; execution.status = 'completed'; execution.endTime = new Date(); execution.duration = execution.endTime.getTime() - execution.startTime.getTime(); execution.metrics.executionTime = execution.duration; this.activeExecutions.delete(execution.id); this.emit('executionCompleted', { execution }); } catch (error) { execution.status = 'failed'; execution.endTime = new Date(); execution.duration = execution.endTime!.getTime() - execution.startTime.getTime(); this.activeExecutions.delete(execution.id); this.emit('executionFailed', { execution, error }); } } private async executeTestsSequentially( tests: TestCase[], runner: TestRunner, execution: TestExecution, suite: TestSuite ): Promise<void> { for (const test of tests) { const result = await this.executeTest(test, runner, suite); execution.results.push(result); this.emit('testCompleted', { execution, result }); } } private async executeTestsInParallel( tests: TestCase[], runner: TestRunner, execution: TestExecution, suite: TestSuite ): Promise<void> { const chunks = this.chunkArray(tests, execution.parallelExecutions); await Promise.all( chunks.map(async (chunk) => { for (const test of chunk) { const result = await this.executeTest(test, runner, suite); execution.results.push(result); this.emit('testCompleted', { execution, result }); } }) ); } private async executeTest(test: TestCase, runner: TestRunner, suite: TestSuite): Promise<TestResult> { const startTime = new Date(); let retries = 0; let lastError: TestError | undefined; while (retries <= test.retries) { try { const result = await runner.executeTest(test, suite); result.retries = retries; return result; } catch (error) { lastError = { message: error.message, stack: error.stack, type: error.constructor.name }; retries++; if (retries <= test.retries) { await this.delay(1000 * retries); // Exponential backoff } } } // Test failed after all retries return { testId: test.id, name: test.name, status: 'error', startTime, endTime: new Date(), duration: Date.now() - startTime.getTime(), steps: [], assertions: [], error: lastError, screenshots: [], logs: [], metrics: { customMetrics: {} }, retries, flaky: retries > 0 }; } private calculateExecutionSummary(execution: TestExecution): void { const summary = execution.summary; summary.passed = execution.results.filter(r => r.status === 'passed').length; summary.failed = execution.results.filter(r => r.status === 'failed').length; summary.skipped = execution.results.filter(r => r.status === 'skipped').length; summary.error = execution.results.filter(r => r.status === 'error').length; summary.flaky = execution.results.filter(r => r.flaky).length; summary.duration = execution.results.reduce((sum, r) => sum + r.duration, 0); summary.passRate = summary.total > 0 ? summary.passed / summary.total : 0; } private async setupTestEnvironment(suite: TestSuite, execution: TestExecution): Promise<void> { // Setup database if (suite.setup.database) { await this.setupDatabase(suite.setup.database); } // Start services if (suite.setup.services) { for (const service of suite.setup.services) { await this.startService(service); } } // Run setup scripts for (const script of suite.setup.scripts) { await this.runScript(script); } } private async teardownTestEnvironment(suite: TestSuite, execution: TestExecution): Promise<void> { // Run teardown scripts for (const script of suite.teardown.scripts) { await this.runScript(script); } // Archive logs if (suite.teardown.archiveLogs) { await this.archiveLogs(execution); } // Reset database if (suite.teardown.resetDatabase) { await this.resetDatabase(); } // Stop services if (suite.teardown.stopServices) { await this.stopServices(); } } private async validateTestSuite(suite: TestSuite): Promise<void> { // Validate test framework is supported if (!this.testRunners.has(suite.framework)) { throw new Error(`Unsupported test framework: ${suite.framework}`); } // Validate test cases for (const test of suite.tests) { if (!test.steps.length && test.automated) { throw new Error(`Automated test ${test.name} must have steps`); } } // Validate dependencies for (const dep of suite.dependencies) { if (!this.testSuites.has(dep)) { throw new Error(`Dependency ${dep} not found`); } } } private async evaluateQualityGate(gate: QualityGate, execution: TestExecution): Promise<QualityGateResult> { const result: QualityGateResult = { gateId: gate.id, name: gate.name, passed: true, conditions: [], enforcement: gate.enforcement }; for (const condition of gate.conditions) { const conditionResult = await this.evaluateQualityCondition(condition, execution); result.conditions.push(conditionResult); if (!conditionResult.passed && condition.severity === 'blocker') { result.passed = false; } } return result; } private async evaluateQualityCondition( condition: QualityCondition, execution: TestExecution ): Promise<QualityConditionResult> { let actualValue: number = 0; // Get actual value based on metric switch (condition.metric) { case 'pass_rate': actualValue = execution.summary.passRate; break; case 'coverage': actualValue = execution.coverage?.overall || 0; break; case 'duration': actualValue = execution.duration || 0; break; case 'error_rate': actualValue = execution.summary.error / Math.max(execution.summary.total, 1); break; case 'flaky_rate': actualValue = execution.summary.flaky / Math.max(execution.summary.total, 1); break; } const passed = this.evaluateCondition(actualValue, condition.operator, condition.threshold); return { conditionId: this.generateId(), metric: condition.metric, operator: condition.operator, threshold: condition.threshold, actualValue, passed, severity: condition.severity, message: passed ? 'Condition met' : condition.errorMessage }; } private evaluateCondition(actual: number, operator: string, threshold: number): boolean { switch (operator) { case 'gt': return actual > threshold; case 'gte': return actual >= threshold; case 'lt': return actual < threshold; case 'lte': return actual <= threshold; case 'eq': return actual === threshold; case 'ne': return actual !== threshold; default: return false; } } private async generateReportContent( execution: TestExecution, type: string, format: string ): Promise<string> { switch (format) { case 'json': return JSON.stringify(execution, null, 2); case 'html': return this.generateHTMLReport(execution, type); case 'junit': return this.generateJUnitReport(execution); default: return JSON.stringify(execution, null, 2); } } private generateHTMLReport(execution: TestExecution, type: string): string { return ` <!DOCTYPE html> <html> <head> <title>Test Report - ${execution.name}</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .summary { background: #f5f5f5; padding: 15px; border-radius: 5px; } .passed { color: green; } .failed { color: red; } .error { color: orange; } table { border-collapse: collapse; width: 100%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } </style> </head> <body> <h1>Test Report: ${execution.name}</h1> <div class="summary"> <h2>Summary</h2> <p><strong>Status:</strong> ${execution.status}</p> <p><strong>Duration:</strong> ${execution.duration}ms</p> <p><strong>Pass Rate:</strong> ${(execution.summary.passRate * 100).toFixed(1)}%</p> <p><span class="passed">Passed: ${execution.summary.passed}</span> | <span class="failed">Failed: ${execution.summary.failed}</span> | <span class="error">Errors: ${execution.summary.error}</span></p> </div> <table> <thead> <tr> <th>Test Name</th> <th>Status</th> <th>Duration</th> <th>Retries</th> </tr> </thead> <tbody> ${execution.results.map(result => ` <tr> <td>${result.name}</td> <td class="${result.status}">${result.status}</td> <td>${result.duration}ms</td> <td>${result.retries}</td> </tr> `).join('')} </tbody> </table> </body> </html>`; } private generateJUnitReport(execution: TestExecution): string { const xmlResults = execution.results.map(result => ` <testcase classname="${execution.name}" name="${result.name}" time="${result.duration / 1000}"> ${result.status === 'failed' ? `<failure message="${result.error?.message || 'Test failed'}">${result.error?.stack || ''}</failure>` : ''} ${result.status === 'error' ? `<error message="${result.error?.message || 'Test error'}">${result.error?.stack || ''}</error>` : ''} ${result.status === 'skipped' ? '<skipped/>' : ''} </testcase> `).join(''); return `<?xml version="1.0" encoding="UTF-8"?> <testsuite name="${execution.name}" tests="${execution.summary.total}" failures="${execution.summary.failed}" errors="${execution.summary.error}" skipped="${execution.summary.skipped}" time="${(execution.duration || 0) / 1000}"> ${xmlResults} </testsuite>`; } private async generateCoverageReport(execution: TestExecution, suite: TestSuite): Promise<CoverageReport> { // Mock coverage report return { overall: 85.5, byFile: {}, byFunction: {}, byLine: {}, thresholdMet: 85.5 >= suite.coverage.threshold, reportUrl: `/coverage/${execution.id}/index.html` }; } private async setupDatabase(config: DatabaseSetup): Promise<void> { // Database setup implementation } private async startService(config: ServiceSetup): Promise<void> { // Service startup implementation } private async runScript(script: string): Promise<void> { // Script execution implementation } private async archiveLogs(execution: TestExecution): Promise<void> { // Log archival implementation } private async resetDatabase(): Promise<void> { // Database reset implementation } private async stopServices(): Promise<void> { // Service stop implementation } private chunkArray<T>(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private initializeTestRunners(): void { // Initialize test runners for different frameworks this.testRunners.set('jest', new JestTestRunner()); this.testRunners.set('cypress', new CypressTestRunner()); this.testRunners.set('playwright', new PlaywrightTestRunner()); this.testRunners.set('k6', new K6TestRunner()); } private startScheduler(): void { setInterval(() => { this.processScheduledTests(); }, 60000); // Every minute } private startMetricsCollection(): void { setInterval(() => { this.collectMetrics(); }, 30000); // Every 30 seconds } private async processScheduledTests(): Promise<void> { const now = new Date(); for (const suite of this.testSuites.values()) { if (suite.schedule?.enabled && this.shouldRunScheduledTest(suite.schedule, now)) { await this.executeTestSuite(suite.id, { triggeredBy: 'scheduler', trigger: 'schedule' }); } } } private shouldRunScheduledTest(schedule: TestSchedule, now: Date): boolean { // Simplified cron evaluation - in production use proper cron library return Math.random() > 0.99; // 1% chance per minute for demo } private collectMetrics(): void { // Collect system metrics const activeCount = this.activeExecutions.size; const totalExecutions = this.executions.size; this.emit('metricsCollected', { activeExecutions: activeCount, totalExecutions, timestamp: new Date() }); } private generateId(): string { return Math.random().toString(36).substring(2, 15); } } // Test Runner Interfaces interface TestRunner { executeTest(test: TestCase, suite: TestSuite): Promise<TestResult>; } class JestTestRunner implements TestRunner { async executeTest(test: TestCase, suite: TestSuite): Promise<TestResult> { const startTime = new Date(); // Simulate Jest test execution await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 500)); const passed = Math.random() > 0.1; // 90% pass rate return { testId: test.id, name: test.name, status: passed ? 'passed' : 'failed', startTime, endTime: new Date(), duration: Date.now() - startTime.getTime(), steps: [], assertions: test.assertions.map(a => ({ assertionId: a.id, status: passed ? 'passed' : 'failed', expected: a.expected, actual: passed ? a.expected : 'different value' })), screenshots: [], logs: [`Test ${test.name} ${passed ? 'passed' : 'failed'}`], metrics: { customMetrics: {} }, retries: 0, flaky: false }; } } class CypressTestRunner implements TestRunner { async executeTest(test: TestCase, suite: TestSuite): Promise<TestResult> { const startTime = new Date(); // Simulate Cypress test execution with screenshots await new Promise(resolve => setTimeout(resolve, Math.random() * 3000 + 1000)); const passed = Math.random() > 0.15; // 85% pass rate return { testId: test.id, name: test.name, status: passed ? 'passed' : 'failed', startTime, endTime: new Date(), duration: Date.now() - startTime.getTime(), steps: test.steps.map(step => ({ stepId: step.id, description: step.description, status: passed ? 'passed' : 'failed', startTime: new Date(), endTime: new Date(), duration: 100, screenshot: '/screenshots/step-' + step.id + '.png' })), assertions: [], screenshots: ['/screenshots/final-' + test.id + '.png'], logs: [`Cypress test ${test.name} executed`], metrics: { customMetrics: {} }, retries: 0, flaky: false }; } } class PlaywrightTestRunner implements TestRunner { async executeTest(test: TestCase, suite: TestSuite): Promise<TestResult> { const startTime = new Date(); // Simulate Playwright test execution await new Promise(resolve => setTimeout(resolve, Math.random() * 2500 + 750)); const passed = Math.random() > 0.12; // 88% pass rate return { testId: test.id, name: test.name, status: passed ? 'passed' : 'failed', startTime, endTime: new Date(), duration: Date.now() - startTime.getTime(), steps: [], assertions: [], screenshots: [], logs: [`Playwright test ${test.name} executed`], metrics: { responseTime: Math.random() * 500 + 100, customMetrics: {} }, retries: 0, flaky: false }; } } class K6TestRunner implements TestRunner { async executeTest(test: TestCase, suite: TestSuite): Promise<TestResult> { const startTime = new Date(); // Simulate K6 performance test await new Promise(resolve => setTimeout(resolve, Math.random() * 5000 + 2000)); const responseTime = Math.random() * 200 + 50; const passed = responseTime < 500; // Pass if response time < 500ms return { testId: test.id, name: test.name, status: passed ? 'passed' : 'failed', startTime, endTime: new Date(), duration: Date.now() - startTime.getTime(), steps: [], assertions: [], screenshots: [], logs: [`K6 performance test completed`], metrics: { responseTime, customMetrics: { 'requests_per_second': Math.random() * 1000 + 500, 'error_rate': Math.random() * 0.05 } }, retries: 0, flaky: false }; } } // Helper interfaces interface QualityGateResult { gateId: string; name: string; passed: boolean; conditions: QualityConditionResult[]; enforcement: string; } interface QualityConditionResult { conditionId: string; metric: string; operator: string; threshold: number; actualValue: number; passed: boolean; severity: string; message: string; }

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/Coder-RL/Claude_MCPServer_Dev1'

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