@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
426 lines (344 loc) • 12.9 kB
text/typescript
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;
}
}