managarten/scripts/test-reporting/aggregate-coverage.js
Wuesteon 304897261d test: implement comprehensive automated testing system with daily CI/CD
Implement rock-solid automated testing infrastructure for mana-core-auth
with daily execution, notifications, and comprehensive monitoring.

Test Suite Improvements:
- Fix all 36 failing BetterAuthService tests (missing service mocks)
- Add 21 JwtAuthGuard tests achieving 100% statement coverage
- Create silentError helper to suppress intentional error logs
- Fix Todo backend TaskService test structure
- Add jose mock for JWT testing
- Configure jest collectCoverageFrom for mana-core-auth

GitHub Actions Workflow:
- Daily automated test execution (2 AM UTC + manual trigger)
- Matrix parallelization across 6 backend services
- PostgreSQL and Redis service containers
- Coverage enforcement (80% threshold)
- Multi-channel notifications (Discord, Slack, GitHub Issues)
- Support for success notifications (opt-in)

Test Infrastructure:
- Coverage aggregation across multiple services
- Flaky test detection with 30-run history tracking
- Performance metrics tracking with regression detection
- Test data seeding and cleanup scripts
- Comprehensive test reporting with formatted metrics

Documentation:
- TESTING_GUIDE.md (4000+ words) - Complete testing documentation
- AUTOMATED_TESTING_SYSTEM.md - System architecture and workflows
- DISCORD_NOTIFICATIONS_SETUP.md - Discord webhook setup guide
- TESTING_DEPLOYMENT_CHECKLIST.md - Pre-deployment verification
- TESTING_QUICK_REFERENCE.md - Quick command reference

Final Result:
- 180/180 tests passing (100% pass rate)
- Zero console errors in test output
- Automated daily testing with rich notifications
- Production-ready test infrastructure
2025-12-25 19:12:27 +01:00

158 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
/**
* Aggregate Coverage Reports
*
* Merges multiple coverage reports from different test suites into a single
* aggregated report for overall project coverage analysis.
*
* Usage:
* node aggregate-coverage.js <input-dir> <output-dir>
*/
const fs = require('fs');
const path = require('path');
function findCoverageFiles(dir) {
const coverageFiles = [];
function walk(currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.name === 'coverage-summary.json') {
coverageFiles.push(fullPath);
}
}
}
walk(dir);
return coverageFiles;
}
function mergeCoverage(coverageFiles) {
const merged = {
total: {
lines: { total: 0, covered: 0, skipped: 0, pct: 0 },
statements: { total: 0, covered: 0, skipped: 0, pct: 0 },
functions: { total: 0, covered: 0, skipped: 0, pct: 0 },
branches: { total: 0, covered: 0, skipped: 0, pct: 0 },
},
suites: {},
};
for (const file of coverageFiles) {
const content = JSON.parse(fs.readFileSync(file, 'utf8'));
const suiteName = path.basename(path.dirname(path.dirname(file)));
// Store individual suite data
merged.suites[suiteName] = content.total;
// Aggregate totals
if (content.total) {
['lines', 'statements', 'functions', 'branches'].forEach((metric) => {
merged.total[metric].total += content.total[metric].total || 0;
merged.total[metric].covered += content.total[metric].covered || 0;
merged.total[metric].skipped += content.total[metric].skipped || 0;
});
}
}
// Calculate percentages
['lines', 'statements', 'functions', 'branches'].forEach((metric) => {
if (merged.total[metric].total > 0) {
merged.total[metric].pct = (merged.total[metric].covered / merged.total[metric].total) * 100;
}
});
return merged;
}
function generateMarkdownSummary(coverage) {
let markdown = '# Test Coverage Summary\n\n';
// Overall coverage table
markdown += '## Overall Coverage\n\n';
markdown += '| Metric | Coverage | Total | Covered |\n';
markdown += '|--------|----------|-------|--------|\n';
['lines', 'statements', 'functions', 'branches'].forEach((metric) => {
const data = coverage.total[metric];
const pct = data.pct.toFixed(2);
const icon = data.pct >= 80 ? '✅' : data.pct >= 60 ? '⚠️' : '❌';
markdown += `| ${metric.charAt(0).toUpperCase() + metric.slice(1)} | ${icon} ${pct}% | ${data.total} | ${data.covered} |\n`;
});
// Per-suite breakdown
markdown += '\n## Coverage by Test Suite\n\n';
markdown += '| Suite | Lines | Statements | Functions | Branches |\n';
markdown += '|-------|-------|------------|-----------|----------|\n';
Object.entries(coverage.suites).forEach(([suite, data]) => {
const linesPct = data.lines.pct.toFixed(1);
const stmtPct = data.statements.pct.toFixed(1);
const funcPct = data.functions.pct.toFixed(1);
const branchPct = data.branches.pct.toFixed(1);
markdown += `| ${suite} | ${linesPct}% | ${stmtPct}% | ${funcPct}% | ${branchPct}% |\n`;
});
return markdown;
}
function main() {
const inputDir = process.argv[2];
const outputDir = process.argv[3];
if (!inputDir || !outputDir) {
console.error('Usage: node aggregate-coverage.js <input-dir> <output-dir>');
process.exit(1);
}
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Find all coverage files
console.log(`Searching for coverage files in ${inputDir}...`);
const coverageFiles = findCoverageFiles(inputDir);
console.log(`Found ${coverageFiles.length} coverage files`);
if (coverageFiles.length === 0) {
console.log('No coverage files found. Skipping aggregation.');
process.exit(0);
}
// Merge coverage data
console.log('Merging coverage data...');
const merged = mergeCoverage(coverageFiles);
// Write aggregated coverage
const outputFile = path.join(outputDir, 'total-coverage.json');
fs.writeFileSync(outputFile, JSON.stringify(merged, null, 2));
console.log(`Wrote aggregated coverage to ${outputFile}`);
// Generate markdown summary
const summary = generateMarkdownSummary(merged);
const summaryFile = path.join(outputDir, 'summary.md');
fs.writeFileSync(summaryFile, summary);
console.log(`Wrote summary to ${summaryFile}`);
// Output summary to console
console.log('\n' + summary);
// Exit with error if coverage is too low
if (merged.total.lines.pct < 80) {
console.error(`\n❌ Coverage ${merged.total.lines.pct.toFixed(2)}% is below 80% threshold`);
process.exit(1);
}
console.log(`\n✅ Coverage ${merged.total.lines.pct.toFixed(2)}% meets 80% threshold`);
}
main();