- 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>
267 lines
11 KiB
JavaScript
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 }; |