UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

426 lines (344 loc) 12.9 kB
import { StorageManager } from '../../storage/storage-manager.js'; import { ConfigManager } from '../../config/config-manager.js'; import crypto from 'crypto'; import { TestResult, TestHistory, TestComparison, CoverageReport, FlakyTest, TestBaseline, } from './types.js'; export class TestingStore { private storageManager: StorageManager; private moduleName = 'testing-framework'; constructor(configManager: ConfigManager) { this.storageManager = new StorageManager(); } async initialize(): Promise<void> { await this.storageManager.ensureStorageDirectories(); } // Test Result Management async saveTestResult(result: TestResult): Promise<void> { let history: TestResult[] = []; try { history = await this.storageManager.loadData(this.moduleName, 'test-history.json') || []; } catch { // File doesn't exist yet } history.push(result); // Keep only last 100 test runs if (history.length > 100) { history = history.slice(-100); } await this.storageManager.saveData(this.moduleName, 'test-history.json', history); } async getTestHistory(limit?: number): Promise<TestResult[]> { try { const history = await this.storageManager.loadData(this.moduleName, 'test-history.json') as TestResult[]; if (limit && limit > 0) { return (history || []).slice(-limit); } return history || []; } catch { return []; } } async getLatestTestResult(): Promise<TestResult | null> { const history = await this.getTestHistory(1); return history.length > 0 ? history[0] : null; } // Coverage Management async saveCoverageData(coverage: any): Promise<void> { const filename = `coverage-${new Date().toISOString().split('T')[0]}.json`; await this.storageManager.saveData(this.moduleName, filename, coverage); // Also update latest coverage await this.storageManager.saveData(this.moduleName, 'coverage-latest.json', coverage); } async getLatestCoverage(): Promise<any | null> { try { return await this.storageManager.loadData(this.moduleName, 'coverage-latest.json'); } catch { return null; } } async getCoverageHistory(days: number = 30): Promise<any[]> { const history: any[] = []; const startDate = new Date(); startDate.setDate(startDate.getDate() - days); // This is simplified - in a real implementation, you'd scan for coverage files // For now, just return the latest coverage if it exists const latest = await this.getLatestCoverage(); if (latest) { history.push(latest); } return history; } // Flaky Test Detection async trackFlakyTest(testName: string, suiteName: string, passed: boolean): Promise<void> { let flakyTests: Record<string, FlakyTest> = {}; try { flakyTests = await this.storageManager.loadData(this.moduleName, 'flaky-tests.json') || {}; } catch { // File doesn't exist yet } const key = `${suiteName}::${testName}`; if (!flakyTests[key]) { flakyTests[key] = { testId: crypto.randomUUID(), name: testName, suite: suiteName, failureRate: 0, recentResults: [], lastFailed: new Date().toISOString(), errorPatterns: [], testName, suiteName, firstSeen: new Date().toISOString(), lastSeen: new Date().toISOString(), failures: 0, passes: 0, totalRuns: 0, recentRuns: [], }; } const test = flakyTests[key]; test.lastSeen = new Date().toISOString(); test.totalRuns++; if (passed) { test.passes++; } else { test.failures++; } test.failureRate = test.failures / test.totalRuns; // Track recent runs (last 10) test.recentRuns.push({ passed, timestamp: new Date().toISOString(), }); if (test.recentRuns.length > 10) { test.recentRuns = test.recentRuns.slice(-10); } await this.storageManager.saveData(this.moduleName, 'flaky-tests.json', flakyTests); } async getFlakyTests(threshold: number = 0.1): Promise<FlakyTest[]> { try { const flakyTests = await this.storageManager.loadData(this.moduleName, 'flaky-tests.json') as Record<string, FlakyTest>; return Object.values(flakyTests || {}) .filter(test => test.failureRate > threshold && test.failureRate < (1 - threshold) && test.totalRuns >= 5 ) .sort((a, b) => b.failureRate - a.failureRate); } catch { return []; } } // Test Baselines async setTestBaseline(name: string, baseline: TestBaseline): Promise<void> { let baselines: Record<string, TestBaseline> = {}; try { baselines = await this.storageManager.loadData(this.moduleName, 'test-baselines.json') || {}; } catch { // File doesn't exist yet } baselines[name] = baseline; await this.storageManager.saveData(this.moduleName, 'test-baselines.json', baselines); } async getTestBaseline(name: string): Promise<TestBaseline | null> { try { const baselines = await this.storageManager.loadData(this.moduleName, 'test-baselines.json') as Record<string, TestBaseline>; return baselines[name] || null; } catch { return null; } } async getAllBaselines(): Promise<Record<string, TestBaseline>> { try { return await this.storageManager.loadData(this.moduleName, 'test-baselines.json') || {}; } catch { return {}; } } // Additional methods for testing-framework module async getTestHistoryForProject(projectId: string): Promise<TestHistory> { const history = await this.getTestHistory(); const projectHistory = history.filter(r => r.projectId === projectId); // Calculate trends const trends = []; if (projectHistory.length >= 2) { const recent = projectHistory.slice(-10); const successRates = recent.map(r => r.summary.successRate); const avgSuccessRate = successRates.reduce((a, b) => a + b, 0) / successRates.length; const firstRate = successRates[0]; const lastRate = successRates[successRates.length - 1]; trends.push({ metric: 'successRate' as const, values: successRates.map((v, i) => ({ timestamp: recent[i].timestamp, value: v })), trend: lastRate > firstRate ? 'improving' as const : lastRate < firstRate ? 'degrading' as const : 'stable' as const, changePercentage: ((lastRate - firstRate) / firstRate) * 100, }); } return { projectId, results: projectHistory, baseline: projectHistory.find(r => r.tags?.includes('baseline')), trends, lastUpdated: new Date().toISOString(), }; } async compareTestResults(baselineId: string, currentId: string): Promise<TestComparison> { const history = await this.getTestHistory(); const baseline = history.find(r => r.id === baselineId); const current = history.find(r => r.id === currentId); if (!baseline || !current) { throw new Error('Test results not found'); } const baselineTests = new Set(baseline.suites.flatMap(s => s.tests.map(t => `${s.name}::${t.name}`))); const currentTests = new Set(current.suites.flatMap(s => s.tests.map(t => `${s.name}::${t.name}`))); return { baseline, current, improvements: [], regressions: [], newTests: Array.from(currentTests).filter(t => !baselineTests.has(t)), removedTests: Array.from(baselineTests).filter(t => !currentTests.has(t)), coverageChange: { lines: (current.coverage?.lines.percentage || 0) - (baseline.coverage?.lines.percentage || 0), statements: (current.coverage?.statements.percentage || 0) - (baseline.coverage?.statements.percentage || 0), functions: (current.coverage?.functions.percentage || 0) - (baseline.coverage?.functions.percentage || 0), branches: (current.coverage?.branches.percentage || 0) - (baseline.coverage?.branches.percentage || 0), }, }; } async saveFlakyTests(tests: FlakyTest[]): Promise<void> { await this.storageManager.saveData(this.moduleName, 'flaky-tests-report.json', { flakyTests: tests, timestamp: new Date().toISOString(), }); } async setBaseline(resultId: string, projectId: string): Promise<void> { const history = await this.getTestHistory(); const result = history.find(r => r.id === resultId); if (!result) { throw new Error('Test result not found'); } // Add baseline tag if (!result.tags) { result.tags = []; } if (!result.tags.includes('baseline')) { result.tags.push('baseline'); } // Update history await this.storageManager.saveData(this.moduleName, 'test-history.json', history); } async getTestResultById(id: string): Promise<TestResult | null> { const history = await this.getTestHistory(); return history.find(r => r.id === id) || null; } // Test History Analysis async getTestTrend(testName: string, days: number = 7): Promise<Array<{ timestamp: string; passed: boolean; duration?: number; }>> { const history = await this.getTestHistory(); const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const trend: Array<{ timestamp: string; passed: boolean; duration?: number }> = []; for (const result of history) { if (new Date(result.timestamp) < startDate) continue; const suite = result.suites.find(s => s.tests.some(t => t.name === testName) ); if (suite) { const test = suite.tests.find(t => t.name === testName); if (test) { trend.push({ timestamp: result.timestamp, passed: test.status === 'passed', duration: test.duration, }); } } } return trend; } async getSuccessRate(days: number = 30): Promise<number> { const history = await this.getTestHistory(); const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const recentRuns = history.filter(result => new Date(result.timestamp) >= startDate ); if (recentRuns.length === 0) return 0; const successfulRuns = recentRuns.filter(result => result.summary.failed === 0 ).length; return (successfulRuns / recentRuns.length) * 100; } // Export/Import async exportTestData(): Promise<any> { const [ history, coverage, flakyTests, baselines, ] = await Promise.all([ this.getTestHistory(), this.getLatestCoverage(), this.storageManager.loadData(this.moduleName, 'flaky-tests.json').catch(() => ({})), this.getAllBaselines(), ]); return { testHistory: history, latestCoverage: coverage, flakyTests, testBaselines: baselines, exportedAt: new Date().toISOString(), }; } async importTestData(data: any): Promise<void> { if (data.testHistory) { await this.storageManager.saveData(this.moduleName, 'test-history.json', data.testHistory); } if (data.latestCoverage) { await this.storageManager.saveData(this.moduleName, 'coverage-latest.json', data.latestCoverage); } if (data.flakyTests) { await this.storageManager.saveData(this.moduleName, 'flaky-tests.json', data.flakyTests); } if (data.testBaselines) { await this.storageManager.saveData(this.moduleName, 'test-baselines.json', data.testBaselines); } } // Cleanup async cleanupOldData(daysToKeep: number = 30): Promise<number> { let cleanedItems = 0; // Clean test history const history = await this.getTestHistory(); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); const filteredHistory = history.filter(result => new Date(result.timestamp) > cutoffDate ); if (filteredHistory.length < history.length) { await this.storageManager.saveData(this.moduleName, 'test-history.json', filteredHistory); cleanedItems += history.length - filteredHistory.length; } // Clean flaky test data that hasn't been seen recently const flakyTests = await this.storageManager.loadData(this.moduleName, 'flaky-tests.json').catch(() => ({})) as Record<string, FlakyTest>; const filteredFlakyTests: Record<string, FlakyTest> = {}; for (const [key, test] of Object.entries(flakyTests)) { if (new Date(test.lastSeen) > cutoffDate) { filteredFlakyTests[key] = test; } else { cleanedItems++; } } await this.storageManager.saveData(this.moduleName, 'flaky-tests.json', filteredFlakyTests); return cleanedItems; } }