// ============================================================================ // Agent 8: Report Generator // Creates comprehensive reports and Testing Protocol (Changelog-style) // ============================================================================ import * as path from 'path'; import * as fs from 'fs'; import chalk from 'chalk'; import boxen from 'boxen'; import figures from 'figures'; import { AgentState, TestSuiteReport, ReportSummary, } from '../types'; import { writeFileContent, ensureDir, readFileContent } from '../utils/file-utils'; import { logger } from '../utils/logger'; // Zurich timezone identifier const ZURICH_TZ = 'Europe/Zurich'; /** * Formats a Date to Zurich timezone with exact date and time. */ function zurichDate(date: Date = new Date()): { dateStr: string; timeStr: string; full: string } { const opts: Intl.DateTimeFormatOptions = { timeZone: ZURICH_TZ }; const dateStr = date.toLocaleDateString('de-CH', { ...opts, weekday: 'long', year: 'numeric', month: '2-digit', day: '2-digit', }); const timeStr = date.toLocaleTimeString('de-CH', { ...opts, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const full = `${dateStr}, ${timeStr} (Zurich)`; return { dateStr, timeStr, full }; } /** * Short date for section headers: "17.02.2026" */ function zurichShortDate(date: Date = new Date()): string { return date.toLocaleDateString('de-CH', { timeZone: ZURICH_TZ, year: 'numeric', month: '2-digit', day: '2-digit', }); } /** * Short time: "14:32:05" */ function zurichTime(date: Date = new Date()): string { return date.toLocaleTimeString('de-CH', { timeZone: ZURICH_TZ, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } export async function reporterAgent(state: AgentState): Promise> { const startTime = Date.now(); logger.agentStart('reporter'); try { const reportsDir = path.resolve(state.projectPath, state.config.reportsDir); ensureDir(reportsDir); const now = new Date(); const timestamp = now.toISOString(); const projectName = state.projectStructure?.name || 'Unknown Project'; // Build summary const summary = buildSummary(state); // Build full report const report: TestSuiteReport = { projectName, timestamp, duration: Date.now() - startTime, structure: state.projectStructure!, analysis: state.codeAnalysis!, strategy: state.testStrategy!, generatedTests: state.generatedTests, reviews: state.testReviews, results: state.testResults, security: state.securityReport!, summary, }; // 1. Generate JSON report logger.agent('reporter', 'Creating JSON report...'); const jsonPath = path.join(reportsDir, `report-${formatTimestamp(timestamp)}.json`); writeFileContent(jsonPath, JSON.stringify(report, null, 2)); // 2. Generate Markdown report logger.agent('reporter', 'Creating Markdown report...'); const mdContent = generateMarkdownReport(report); const mdPath = path.join(reportsDir, `report-${formatTimestamp(timestamp)}.md`); writeFileContent(mdPath, mdContent); // 3. Generate HTML report logger.agent('reporter', 'Creating HTML report...'); const htmlContent = generateHtmlReport(report); const htmlPath = path.join(reportsDir, `report-${formatTimestamp(timestamp)}.html`); writeFileContent(htmlPath, htmlContent); // 4. Testing Protocol — persistent changelog, newest entries on top logger.agent('reporter', 'Updating testing protocol...'); updateTestingProtocol(reportsDir, state, report, now); // Print final summary to console printFinalSummary(report); logger.agent('reporter', `Reports saved to: ${reportsDir}`); logger.agentComplete('reporter', Date.now() - startTime); return { report, status: 'completed', agentLog: [ ...state.agentLog, { agent: 'reporter', timestamp: new Date().toISOString(), action: 'Reports created', details: `JSON, Markdown, HTML, Testing Protocol in ${reportsDir}`, duration: Date.now() - startTime, status: 'complete', }, ], }; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); logger.agentError('reporter', errMsg); return { errors: [...state.errors, `Reporter: ${errMsg}`], agentLog: [ ...state.agentLog, { agent: 'reporter', timestamp: new Date().toISOString(), action: 'Error', details: errMsg, status: 'error', }, ], }; } } // ============================================================================ // Testing Protocol — Changelog-style, newest first, Zurich timezone // ============================================================================ function updateTestingProtocol( reportsDir: string, state: AgentState, report: TestSuiteReport, now: Date, ): void { const protocolPath = path.join(reportsDir, 'testing-protocol.md'); // Read existing protocol content (everything AFTER the header) let existingEntries = ''; const HEADER_MARKER = ''; if (fs.existsSync(protocolPath)) { const existing = readFileContent(protocolPath); const markerIndex = existing.indexOf(HEADER_MARKER); if (markerIndex >= 0) { existingEntries = existing.substring(markerIndex + HEADER_MARKER.length); } } // Build the new entry const newEntry = buildProtocolEntry(state, report, now); // Assemble file: Header + new entry + old entries (newest on top) const fullContent = buildProtocolFile(newEntry, existingEntries, now); writeFileContent(protocolPath, fullContent); } function buildProtocolEntry(state: AgentState, report: TestSuiteReport, now: Date): string { const zd = zurichDate(now); const s = report.summary; const lines: string[] = []; // Count which agents ran const agentsRun = [...new Set(state.agentLog.map(l => l.agent))]; const durationMs = state.agentLog.reduce((sum, l) => sum + (l.duration || 0), 0); const durationStr = durationMs > 0 ? `${(durationMs / 1000).toFixed(1)}s` : 'N/A'; // Version counter from existing entries const versionTag = `v${Date.now()}`; // Status emoji-free indicators const statusTag = s.productionReady ? 'PASSED' : 'NOT PASSED'; const riskTag = state.securityReport?.overallRisk?.toUpperCase() || 'N/A'; // --- Entry block --- lines.push(`## [${zurichShortDate(now)} ${zurichTime(now)}] — ${report.projectName}`); lines.push(''); lines.push(`**Date:** ${zd.dateStr}`); lines.push(`**Time:** ${zd.timeStr} (Timezone: Zurich/Europe)`); lines.push(`**Status:** ${statusTag}`); lines.push(`**Duration:** ${durationStr}`); lines.push(`**Mode:** ${state.config.testTypes.join(', ')}`); lines.push(`**Agents:** ${agentsRun.join(' -> ')}`); lines.push(''); // Test results table lines.push(`### Test Results`); lines.push(''); lines.push(`| Metric | Value |`); lines.push(`|--------|------|`); lines.push(`| Total Tests | ${s.totalTests} |`); lines.push(`| Passed | ${s.totalPassed} |`); lines.push(`| Failed | ${s.totalFailed} |`); lines.push(`| Skipped | ${s.totalSkipped} |`); lines.push(`| Security Score | ${s.securityScore}/100 |`); lines.push(`| Quality Score | ${s.qualityScore}/100 |`); lines.push(`| Risk Assessment | ${riskTag} |`); lines.push(`| Production Ready | ${statusTag} |`); lines.push(''); // What was tested — detailed breakdown lines.push(`### What Was Tested`); lines.push(''); // Generated tests by type if (state.generatedTests.length > 0) { const byType = new Map(); for (const t of state.generatedTests) { if (!byType.has(t.testType)) byType.set(t.testType, []); byType.get(t.testType)!.push(path.basename(t.targetFile)); } for (const [type, files] of byType) { const count = state.generatedTests.filter(t => t.testType === type).reduce((s, t) => s + t.testCount, 0); lines.push(`**${type.toUpperCase()} Tests** (${count} test cases)`); for (const f of files) { lines.push(`- ${f}`); } lines.push(''); } } // Test execution results per file if (state.testResults.length > 0) { lines.push(`### Execution Details`); lines.push(''); for (const result of state.testResults) { const icon = result.failed > 0 ? 'FAIL' : 'PASS'; lines.push(`- **[${icon}]** ${path.basename(result.testFile)} — ${result.passed}/${result.totalTests} passed (${result.duration}ms)`); for (const err of result.errors) { lines.push(` - Error: \`${err.testName}\`: ${err.message.substring(0, 150)}`); } } lines.push(''); } // Security findings if (state.securityReport && state.securityReport.vulnerabilities.length > 0) { lines.push(`### Security Findings`); lines.push(''); const vulns = state.securityReport.vulnerabilities; const grouped = { critical: vulns.filter(v => v.severity === 'critical'), high: vulns.filter(v => v.severity === 'high'), medium: vulns.filter(v => v.severity === 'medium'), low: vulns.filter(v => v.severity === 'low'), }; if (grouped.critical.length > 0) { lines.push(`**CRITICAL (${grouped.critical.length}):**`); for (const v of grouped.critical) lines.push(`- ${v.type}: ${v.description} (${path.basename(v.filePath)}:${v.line})`); lines.push(''); } if (grouped.high.length > 0) { lines.push(`**HIGH (${grouped.high.length}):**`); for (const v of grouped.high) lines.push(`- ${v.type}: ${v.description} (${path.basename(v.filePath)}:${v.line})`); lines.push(''); } if (grouped.medium.length > 0) { lines.push(`**MEDIUM (${grouped.medium.length}):**`); for (const v of grouped.medium.slice(0, 10)) lines.push(`- ${v.type}: ${v.description} (${path.basename(v.filePath)}:${v.line})`); if (grouped.medium.length > 10) lines.push(`- ... and ${grouped.medium.length - 10} more`); lines.push(''); } if (grouped.low.length > 0) { lines.push(`**LOW (${grouped.low.length}):**`); for (const v of grouped.low.slice(0, 5)) lines.push(`- ${v.type}: ${v.description} (${path.basename(v.filePath)}:${v.line})`); if (grouped.low.length > 5) lines.push(`- ... and ${grouped.low.length - 5} more`); lines.push(''); } } // OWASP check results if (state.securityReport?.owaspChecks) { const failed = state.securityReport.owaspChecks.filter(c => !c.passed); if (failed.length > 0) { lines.push(`### OWASP Top 10 — Failed`); lines.push(''); for (const check of failed) { lines.push(`- **${check.category}**: ${check.findings.slice(0, 2).join(', ')}`); } lines.push(''); } } // Errors during run if (state.errors.length > 0) { lines.push(`### Errors During Execution`); lines.push(''); for (const err of state.errors) { lines.push(`- ${err}`); } lines.push(''); } // Recommendations if (s.recommendations.length > 0) { lines.push(`### Recommendations`); lines.push(''); for (const rec of s.recommendations) { lines.push(`- ${rec}`); } lines.push(''); } // Agent timeline lines.push(`### Agent Timeline`); lines.push(''); lines.push(`| Time | Agent | Action | Duration |`); lines.push(`|------|-------|--------|-------|`); for (const log of state.agentLog) { const logTime = zurichTime(new Date(log.timestamp)); const dur = log.duration ? `${(log.duration / 1000).toFixed(1)}s` : '-'; lines.push(`| ${logTime} | ${log.agent} | ${log.action} | ${dur} |`); } lines.push(''); lines.push('---'); lines.push(''); return lines.join('\n'); } function buildProtocolFile(newEntry: string, existingEntries: string, now: Date): string { const zd = zurichDate(now); const header: string[] = []; header.push(`# Testing Protocol`); header.push(''); header.push(`> Auto-generated by AI Testing Suite`); header.push(`> Timezone: Europe/Zurich (CET/CEST)`); header.push(`> Newest entries on top`); header.push(''); header.push(`**Last updated:** ${zd.full}`); header.push(''); header.push('---'); header.push(''); header.push(''); // New entry on top, then existing entries return header.join('\n') + '\n' + newEntry + existingEntries; } // ============================================================================ // Existing report generators (unchanged) // ============================================================================ function buildSummary(state: AgentState): ReportSummary { const totalTests = state.testResults.reduce((s, r) => s + r.totalTests, 0); const totalPassed = state.testResults.reduce((s, r) => s + r.passed, 0); const totalFailed = state.testResults.reduce((s, r) => s + r.failed, 0); const totalSkipped = state.testResults.reduce((s, r) => s + r.skipped, 0); const coveragePercentage = state.testResults.length > 0 ? state.testResults.reduce((s, r) => s + (r.coverage?.lines || 0), 0) / state.testResults.length : 0; const securityScore = state.securityReport?.score || 0; const qualityScore = state.testReviews.length > 0 ? state.testReviews.reduce((s, r) => s + r.score, 0) / state.testReviews.length : 0; const productionReady = totalFailed === 0 && securityScore >= 70 && qualityScore >= 60; const recommendations: string[] = []; if (totalFailed > 0) recommendations.push(`Fix ${totalFailed} failing tests`); if (securityScore < 70) recommendations.push('Fix security vulnerabilities'); if (qualityScore < 60) recommendations.push('Improve test quality'); if (totalSkipped > 0) recommendations.push(`Implement ${totalSkipped} skipped tests`); const criticalVulns = state.securityReport?.vulnerabilities.filter(v => v.severity === 'critical') || []; if (criticalVulns.length > 0) { recommendations.push(`Fix ${criticalVulns.length} critical vulnerabilities immediately!`); } return { totalTests, totalPassed, totalFailed, totalSkipped, coveragePercentage, securityScore, qualityScore: Math.round(qualityScore), productionReady, recommendations, }; } function generateMarkdownReport(report: TestSuiteReport): string { const s = report.summary; const zd = zurichDate(new Date(report.timestamp)); const lines: string[] = []; lines.push(`# AI Testing Suite - Test Report`); lines.push(''); lines.push(`**Project:** ${report.projectName}`); lines.push(`**Date:** ${zd.full}`); lines.push(`**Production Ready:** ${s.productionReady ? 'YES' : 'NO'}`); lines.push(''); lines.push(`## Summary`); lines.push(''); lines.push(`| Metric | Value |`); lines.push(`|--------|------|`); lines.push(`| Total Tests | ${s.totalTests} |`); lines.push(`| Passed | ${s.totalPassed} |`); lines.push(`| Failed | ${s.totalFailed} |`); lines.push(`| Skipped | ${s.totalSkipped} |`); lines.push(`| Security Score | ${s.securityScore}/100 |`); lines.push(`| Quality Score | ${s.qualityScore}/100 |`); lines.push(''); lines.push(`## Project Structure`); lines.push(''); lines.push(`- **Framework:** ${report.structure.framework.name} (${report.structure.framework.type})`); lines.push(`- **Files:** ${report.structure.totalFiles}`); lines.push(`- **Code Lines:** ${report.structure.totalLines}`); lines.push(`- **Features:** ${report.structure.framework.features.join(', ') || 'None detected'}`); lines.push(''); lines.push(`## Code Analysis`); lines.push(''); lines.push(`| Metric | Value |`); lines.push(`|--------|------|`); lines.push(`| Module | ${report.analysis.modules.length} |`); lines.push(`| Functions | ${report.analysis.metrics.totalFunctions} |`); lines.push(`| Classes | ${report.analysis.metrics.totalClasses} |`); lines.push(`| Interfaces | ${report.analysis.metrics.totalInterfaces} |`); lines.push(`| API Endpoints | ${report.analysis.apiEndpoints.length} |`); lines.push(`| DB Operations | ${report.analysis.databaseOperations.length} |`); lines.push(`| Avg. Complexity | ${report.analysis.metrics.averageComplexity.toFixed(1)} |`); lines.push(''); lines.push(`## Generated Tests`); lines.push(''); const testsByType = new Map(); for (const t of report.generatedTests) { testsByType.set(t.testType, (testsByType.get(t.testType) || 0) + t.testCount); } lines.push(`| Type | Files | Tests |`); lines.push(`|-----|---------|-------|`); for (const [type, count] of testsByType) { const files = report.generatedTests.filter(t => t.testType === type).length; lines.push(`| ${type} | ${files} | ${count} |`); } lines.push(''); if (report.results.length > 0) { lines.push(`## Test Results`); lines.push(''); for (const result of report.results) { const status = result.failed > 0 ? 'FAIL' : 'PASS'; lines.push(`- **${status}** ${path.basename(result.testFile)} - ${result.passed}/${result.totalTests} passed (${result.duration}ms)`); for (const err of result.errors) { lines.push(` - ERROR: ${err.testName}: ${err.message.substring(0, 100)}`); } } lines.push(''); } if (report.security) { lines.push(`## Security Report`); lines.push(''); lines.push(`**Risk Assessment:** ${report.security.overallRisk.toUpperCase()}`); lines.push(`**Score:** ${report.security.score}/100`); lines.push(''); if (report.security.vulnerabilities.length > 0) { lines.push(`### Vulnerabilities (${report.security.vulnerabilities.length})`); lines.push(''); lines.push(`| Severity | Type | File | Description |`); lines.push(`|-------------|-----|-------|-------------|`); for (const v of report.security.vulnerabilities.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity))) { lines.push(`| ${v.severity.toUpperCase()} | ${v.type} | ${path.basename(v.filePath)}:${v.line} | ${v.description} |`); } lines.push(''); } lines.push(`### OWASP Top 10`); lines.push(''); for (const check of report.security.owaspChecks) { const status = check.passed ? 'PASSED' : 'NOT PASSED'; lines.push(`- **${status}** - ${check.category}`); if (!check.passed && check.findings.length > 0) { for (const f of check.findings.slice(0, 3)) { lines.push(` - ${f}`); } } } lines.push(''); } if (s.recommendations.length > 0) { lines.push(`## Recommendations`); lines.push(''); for (const rec of s.recommendations) { lines.push(`1. ${rec}`); } lines.push(''); } lines.push('---'); lines.push(`*Generated by AI Testing Suite | ${zd.full}*`); return lines.join('\n'); } function generateHtmlReport(report: TestSuiteReport): string { const s = report.summary; const zd = zurichDate(new Date(report.timestamp)); const statusColor = s.productionReady ? '#4CAF50' : '#f44336'; const statusText = s.productionReady ? 'PRODUCTION READY' : 'NOT READY'; return ` AI Testing Suite - Report - ${report.projectName}

AI Testing Suite

Test report for ${report.projectName}

${zd.full}

${statusText}

Total Tests

${s.totalTests}
${s.totalPassed} passed / ${s.totalFailed} failed

Security

${s.securityScore}/100

Quality

${s.qualityScore}/100

Vulnerabilities

${report.security?.vulnerabilities.length || 0}
${report.security?.vulnerabilities.filter(v => v.severity === 'critical').length || 0} critical

Project Structure

Framework${report.structure.framework.name} (${report.structure.framework.type})
Files${report.structure.totalFiles}
Code Lines${report.structure.totalLines}
Features${report.structure.framework.features.join(', ') || '-'}
Functions${report.analysis.metrics.totalFunctions}
Classes${report.analysis.metrics.totalClasses}
API Endpoints${report.analysis.apiEndpoints.length}
${report.security && report.security.vulnerabilities.length > 0 ? `

Security Vulnerabilities

${report.security.vulnerabilities .sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity)) .map(v => ``) .join('\n')}
SeverityTypeFileDescription
${v.severity.toUpperCase()}${v.type}${path.basename(v.filePath)}:${v.line}${v.description}
` : ''} ${s.recommendations.length > 0 ? `

Recommendations

    ${s.recommendations.map(r => `
  • ${r}
  • `).join('\n')}
` : ''}
`; } function renderScoreBar(score: number, max: number = 100): string { const barLength = 20; const filled = Math.round((score / max) * barLength); const empty = barLength - filled; const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red; return color('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(empty)); } function printFinalSummary(report: TestSuiteReport): void { const s = report.summary; const zd = zurichDate(new Date(report.timestamp)); logger.header('TEST RESULTS'); // Project info logger.stats('Project', report.projectName); logger.stats('Date/Time', zd.full); logger.stats('Framework', `${report.structure.framework.name} (${report.structure.framework.type})`); logger.divider(); // Test counts logger.stats('Total Tests', s.totalTests); logger.stats('Passed', s.totalPassed); logger.stats('Failed', s.totalFailed); logger.stats('Skipped', s.totalSkipped); logger.divider(); // Score bars console.log(` ${chalk.gray('Security Score:')} ${renderScoreBar(s.securityScore)} ${chalk.white(`${s.securityScore}/100`)}`); console.log(` ${chalk.gray('Quality Score:')} ${renderScoreBar(s.qualityScore)} ${chalk.white(`${s.qualityScore}/100`)}`); logger.newline(); // Production ready panel if (s.productionReady) { console.log( boxen( `${chalk.green(figures.tick)} ${chalk.bold.green('PRODUCTION READY')} - All checks passed!`, { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 1, left: 2, right: 0 }, borderStyle: 'round', borderColor: 'green', } ) ); } else { const lines = [ `${chalk.red(figures.cross)} ${chalk.bold.red('NOT PRODUCTION READY')}`, '', ...s.recommendations.map(rec => `${chalk.yellow(figures.warning)} ${rec}`), ]; console.log( boxen(lines.join('\n'), { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 1, left: 2, right: 0 }, borderStyle: 'round', borderColor: 'red', }) ); } } function formatTimestamp(ts: string): string { return ts.replace(/[:.]/g, '-').replace('T', '_').substring(0, 19); } function severityOrder(severity: string): number { const order: Record = { critical: 0, high: 1, medium: 2, low: 3 }; return order[severity] ?? 4; }