seo-generator-server/tests/reporters/TestReporter.js
StillHammer 4f60de68d6 Fix BatchProcessor initialization and add comprehensive test suite
- Fix BatchProcessor constructor to avoid server blocking during startup
- Add comprehensive integration tests for all modular combinations
- Enhance CLAUDE.md documentation with new test commands
- Update SelectiveLayers configuration for better LLM allocation
- Add AutoReporter system for test automation
- Include production workflow validation tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 14:17:49 +08:00

267 lines
11 KiB
JavaScript

/**
* REPORTER AUTOMATIQUE POUR TESTS D'INTÉGRATION
* Génère automatiquement un rapport HTML détaillé
* S'interface avec le système de logging existant pour capturer les LLM calls
*/
import fs from 'fs';
import path from 'path';
class TestReporter {
constructor() {
this.results = [];
this.startTime = Date.now();
this.currentTest = null;
this.reportDir = path.join(process.cwd(), 'reports');
this.originalConsoleLog = console.log;
// Créer le dossier reports s'il n'existe pas
if (!fs.existsSync(this.reportDir)) {
fs.mkdirSync(this.reportDir, { recursive: true });
}
// Hook dans les logs pour capturer automatiquement les LLM calls
this.hookLogging();
}
hookLogging() {
// Capture automatique des logs JSON pour extraire les stats LLM
const originalStdout = process.stdout.write;
process.stdout.write = (chunk, encoding, callback) => {
const str = chunk.toString();
// Capturer les stats LLM depuis les logs JSON
if (str.includes('"msg":"📊 Stats:') && this.currentTest) {
try {
const logLine = JSON.parse(str.trim());
const statsMatch = logLine.msg.match(/📊 Stats: ({.*})/);
if (statsMatch) {
const stats = JSON.parse(statsMatch[1]);
this.currentTest.llmCalls.push({
provider: stats.provider,
model: stats.model,
duration: stats.duration,
tokens: {
promptTokens: stats.promptTokens,
responseTokens: stats.responseTokens
},
timestamp: stats.timestamp,
input: 'Captured from logs',
output: 'Captured from logs'
});
}
} catch (e) {
// Ignore parsing errors
}
}
return originalStdout.call(process.stdout, chunk, encoding, callback);
};
}
startTest(testName, config = {}) {
this.currentTest = {
name: testName,
startTime: Date.now(),
config,
llmCalls: [],
results: null,
error: null,
status: 'running'
};
}
recordLLMCall(provider, model, input, output, duration, tokens = {}) {
if (!this.currentTest) return;
this.currentTest.llmCalls.push({
provider,
model,
input: typeof input === 'string' ? input.substring(0, 500) + '...' : JSON.stringify(input).substring(0, 500) + '...',
output: typeof output === 'string' ? output.substring(0, 1000) + '...' : JSON.stringify(output).substring(0, 1000) + '...',
duration,
tokens,
timestamp: Date.now()
});
}
endTest(results = null, error = null) {
if (!this.currentTest) return;
this.currentTest.endTime = Date.now();
this.currentTest.duration = this.currentTest.endTime - this.currentTest.startTime;
this.currentTest.results = results;
this.currentTest.error = error;
this.currentTest.status = error ? 'failed' : 'passed';
this.results.push({ ...this.currentTest });
this.currentTest = null;
}
generateReport() {
const totalDuration = Date.now() - this.startTime;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportFile = path.join(this.reportDir, `ti-report-${timestamp}.html`);
const stats = {
total: this.results.length,
passed: this.results.filter(r => r.status === 'passed').length,
failed: this.results.filter(r => r.status === 'failed').length,
totalDuration,
avgDuration: this.results.length ? totalDuration / this.results.length : 0,
totalLLMCalls: this.results.reduce((sum, r) => sum + r.llmCalls.length, 0)
};
const html = this.generateHTML(stats);
fs.writeFileSync(reportFile, html);
// Générer aussi le JSON pour analyse programmatique
const jsonFile = path.join(this.reportDir, `ti-report-${timestamp}.json`);
fs.writeFileSync(jsonFile, JSON.stringify({
timestamp: new Date().toISOString(),
stats,
results: this.results
}, null, 2));
console.log(`\n📊 RAPPORT GÉNÉRÉ AUTOMATIQUEMENT:`);
console.log(` HTML: ${reportFile}`);
console.log(` JSON: ${jsonFile}`);
console.log(` 📈 ${stats.passed}/${stats.total} tests passés | ${stats.totalLLMCalls} appels LLM | ${totalDuration}ms total`);
return reportFile;
}
generateHTML(stats) {
return `<!DOCTYPE html>
<html>
<head>
<title>Rapport Tests d'Intégration - ${new Date().toLocaleString()}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-value { font-size: 2em; font-weight: bold; color: #2196F3; }
.stat-label { color: #666; margin-top: 5px; }
.test-item { background: white; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.test-header { padding: 15px; cursor: pointer; border-bottom: 1px solid #eee; }
.test-header:hover { background: #f9f9f9; }
.test-status { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 10px; }
.status-passed { background: #4CAF50; }
.status-failed { background: #f44336; }
.test-details { padding: 15px; display: none; }
.llm-call { background: #f9f9f9; padding: 10px; margin: 10px 0; border-radius: 4px; border-left: 4px solid #2196F3; }
.config { background: #e3f2fd; padding: 10px; border-radius: 4px; margin: 10px 0; }
.input-output { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.input, .output { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; }
.metrics { display: flex; gap: 20px; margin: 10px 0; }
.metric { background: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Rapport Tests d'Intégration Modulaire</h1>
<p>Généré automatiquement le ${new Date().toLocaleString()}</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-value">${stats.total}</div>
<div class="stat-label">Tests Total</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #4CAF50">${stats.passed}</div>
<div class="stat-label">Tests Réussis</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #f44336">${stats.failed}</div>
<div class="stat-label">Tests Échoués</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.totalLLMCalls}</div>
<div class="stat-label">Appels LLM</div>
</div>
<div class="stat-card">
<div class="stat-value">${Math.round(stats.totalDuration / 1000)}s</div>
<div class="stat-label">Durée Totale</div>
</div>
<div class="stat-card">
<div class="stat-value">${Math.round(stats.avgDuration / 1000)}s</div>
<div class="stat-label">Durée Moyenne</div>
</div>
</div>
<div class="tests">
${this.results.map((test, index) => `
<div class="test-item">
<div class="test-header" onclick="toggleDetails(${index})">
<span class="test-status status-${test.status}"></span>
<strong>${test.name}</strong>
<span style="float: right; color: #666;">${Math.round(test.duration / 1000)}s | ${test.llmCalls.length} LLM calls</span>
</div>
<div class="test-details" id="details-${index}">
${test.config ? `
<div class="config">
<strong>Configuration:</strong>
<pre>${JSON.stringify(test.config, null, 2)}</pre>
</div>
` : ''}
${test.results ? `
<div class="config">
<strong>Résultats:</strong>
<div class="metrics">
${test.results.stats ? Object.entries(test.results.stats).map(([key, value]) =>
`<div class="metric"><strong>${key}:</strong> ${value}</div>`
).join('') : ''}
</div>
</div>
` : ''}
${test.error ? `
<div style="background: #ffebee; padding: 10px; border-radius: 4px; color: #c62828;">
<strong>Erreur:</strong> ${test.error}
</div>
` : ''}
<h4>Appels LLM (${test.llmCalls.length})</h4>
${test.llmCalls.map((call, callIndex) => `
<div class="llm-call">
<div class="metrics">
<div class="metric"><strong>Provider:</strong> ${call.provider}</div>
<div class="metric"><strong>Model:</strong> ${call.model}</div>
<div class="metric"><strong>Durée:</strong> ${call.duration}ms</div>
${call.tokens.promptTokens ? `<div class="metric"><strong>Tokens:</strong> ${call.tokens.promptTokens}${call.tokens.responseTokens}</div>` : ''}
</div>
<div class="input-output">
<div>
<strong>Input:</strong>
<div class="input">${call.input}</div>
</div>
<div>
<strong>Output:</strong>
<div class="output">${call.output}</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
<script>
function toggleDetails(index) {
const details = document.getElementById('details-' + index);
details.style.display = details.style.display === 'none' ? 'block' : 'none';
}
</script>
</body>
</html>`;
}
}
export { TestReporter };