seo-generator-server/tests/reporters/AutoReporter.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

921 lines
39 KiB
JavaScript

/**
* AUTO-REPORTER POUR TESTS - VERSION SIMPLIFIÉE
* Se connecte automatiquement au système de logging pour capturer tout
*/
import fs from 'fs';
import path from 'path';
class AutoReporter {
constructor() {
this.testResults = [];
this.llmCalls = [];
this.phases = []; // Nouveau: tracking des phases
this.currentTestName = null;
this.testStartTime = null;
this.reportDir = path.join(process.cwd(), 'reports');
this.globalStartTime = Date.now();
this.reportGenerated = false;
this.lastActivity = Date.now();
this.timeoutId = null;
// Nouvelles propriétés pour capture prompts/réponses
this.currentLLMCall = null;
this.capturingPrompt = false;
this.capturingResponse = false;
// Créer le dossier reports s'il n'existe pas
if (!fs.existsSync(this.reportDir)) {
fs.mkdirSync(this.reportDir, { recursive: true });
}
this.hookIntoLogs();
this.setupAutoGeneration();
}
setupAutoGeneration() {
// Hook sur la fermeture du processus
process.on('exit', () => {
this.finalize();
});
// Hook sur SIGINT (Ctrl+C)
process.on('SIGINT', () => {
this.finalize();
process.exit(0);
});
// Hook sur SIGTERM
process.on('SIGTERM', () => {
this.finalize();
process.exit(0);
});
// Timer pour générer le rapport après une période de calme
this.startIdleTimer();
}
updateActivity() {
this.lastActivity = Date.now();
// Reset du timer - on reporte la génération
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
// Redémarrer le timer d'inactivité
this.startIdleTimer();
}
startIdleTimer() {
// Générer le rapport après 15 secondes d'inactivité (tests longs avec plusieurs appels LLM)
this.timeoutId = setTimeout(() => {
if (this.testResults.length > 0 && !this.reportGenerated) {
console.log('\n🎯 AutoReporter: Période d\'inactivité détectée, génération du rapport...');
this.finalize();
}
}, 15000);
}
finalize() {
if (this.reportGenerated || this.testResults.length === 0) {
return;
}
try {
this.generateReport();
this.reportGenerated = true;
} catch (error) {
console.error('❌ Erreur lors de la génération du rapport:', error.message);
}
}
hookIntoLogs() {
const originalWrite = process.stdout.write;
const originalError = process.stderr.write;
// Use original write to avoid recursion
const debugLog = (msg) => {
originalWrite.call(process.stdout, msg + '\n');
};
// Function to process test lines (for both stdout and stderr)
const processTestLine = (str) => {
// Capturer résultats de tests depuis les logs JSON avec contexte test
// Les vrais tests ont des appels LLM qui fournissent le contexte
if (str.includes('"msg":"📊 Stats:')) {
try {
const logLine = JSON.parse(str.trim());
const statsMatch = logLine.msg.match(/📊 Stats: ({.*})/);
if (statsMatch) {
const stats = JSON.parse(statsMatch[1]);
const testContext = this.currentTestName;
// Si on a un contexte de test valide, on peut créer/mettre à jour le test
if (testContext && !this.testResults.find(t => t.name === testContext)) {
this.testResults.push({
name: testContext,
status: 'passed', // Si LLM call réussit, test probablement passé
duration: 0, // Sera calculé plus tard
timestamp: Date.now(),
llmCallsCount: 0
});
debugLog(`✅ RAPPORT: Test inféré depuis LLM call - "${testContext}"`);
}
}
} catch (e) {
// Ignore parsing errors
}
this.updateActivity(); // Activité détectée
}
// Capturer aussi les tests Node.js si possible (passés et échoués)
if ((str.includes('✔') || str.includes('✖')) && str.includes('ms)') && !str.includes('JSON') && !str.includes('RAPPORT')) {
const testMatch = str.match(/[✔✖]\s+(.+?)\s+\((\d+(?:\.\d+)?)ms\)/);
if (testMatch) {
const [fullMatch, testName, duration] = testMatch;
const cleanName = testName.trim();
const status = str.includes('✔') ? 'passed' : 'failed';
if (!this.testResults.find(t => t.name === cleanName)) {
this.testResults.push({
name: cleanName,
status: status,
duration: parseFloat(duration),
timestamp: Date.now(),
llmCallsCount: 0
});
debugLog(`✅ RAPPORT: Test Node.js ${status} capturé - "${cleanName}" (${duration}ms)`);
}
}
this.updateActivity(); // Activité détectée
}
if (str.includes('✗') && str.includes('(') && str.includes('ms)')) {
const testMatch = str.match(/✗\s+(.+?)\s+\((\d+(?:\.\d+)?)ms\)/);
if (testMatch) {
const [, testName, duration] = testMatch;
const cleanName = testName.trim();
if (!this.testResults.find(t => t.name === cleanName)) {
this.testResults.push({
name: cleanName,
status: 'failed',
duration: parseFloat(duration),
timestamp: Date.now(),
llmCallsCount: this.llmCalls.filter(call =>
call.testContext && call.testContext.includes(cleanName)
).length
});
debugLog(`📊 Test capturé: ${cleanName} (FAILED ${duration}ms)`);
}
}
this.updateActivity(); // Activité détectée
}
};
process.stdout.write = (chunk, encoding, callback) => {
const str = chunk.toString();
processTestLine(str);
// Capturer stats LLM depuis logs JSON et texte simple
if (str.includes('📊 Stats:')) {
let stats = null;
try {
// Tenter de parser comme JSON d'abord
if (str.includes('"msg":"📊 Stats:')) {
const logLine = JSON.parse(str.trim());
const statsMatch = logLine.msg.match(/📊 Stats: ({.*})/);
if (statsMatch) {
stats = JSON.parse(statsMatch[1]);
}
} else {
// Parser le format texte direct
const statsMatch = str.match(/📊 Stats: ({.*})/);
if (statsMatch) {
stats = JSON.parse(statsMatch[1]);
}
}
if (stats) {
// Créer l'objet LLM call avec les stats + prompt/réponse si disponibles
const llmCall = {
provider: stats.provider,
model: stats.model,
duration: stats.duration,
tokens: {
prompt: stats.promptTokens,
response: stats.responseTokens
},
timestamp: stats.timestamp,
testContext: this.currentTestName || 'unknown'
};
// Ajouter prompt/réponse si capturés
if (this.currentLLMCall) {
if (this.currentLLMCall.prompt) {
llmCall.prompt = this.currentLLMCall.prompt;
}
if (this.currentLLMCall.response) {
llmCall.response = this.currentLLMCall.response;
}
// Reset currentLLMCall
this.currentLLMCall = null;
}
this.llmCalls.push(llmCall);
this.updateActivity(); // Activité détectée
}
} catch (e) {
// Ignore parsing errors
}
}
// Capturer les phases du pipeline
if (str.includes('PHASE') && (str.includes('/4:') || str.includes('1/4') || str.includes('2/4') || str.includes('3/4') || str.includes('4/4'))) {
const phaseMatch = str.match(/PHASE (\d)\/4:\s*([^(\n]+)/);
if (phaseMatch) {
const [, phaseNumber, phaseName] = phaseMatch;
const phase = {
number: parseInt(phaseNumber),
name: phaseName.trim(),
timestamp: new Date().toISOString(),
testContext: this.currentTestName || 'unknown',
status: 'started',
inputElements: [],
outputElements: [],
llmCalls: [],
metrics: {}
};
this.phases.push(phase);
this.updateActivity();
}
}
// Capturer les fins de phases
if (str.includes('✅ Phase') && str.includes('terminé')) {
const phaseEndMatch = str.match(/✅ Phase (\d+):\s*([^(\n]+)/);
if (phaseEndMatch) {
const [, phaseNumber, phaseName] = phaseEndMatch;
// Trouver la phase correspondante et la marquer comme terminée
const phase = this.phases.find(p => p.number === parseInt(phaseNumber) && p.status === 'started');
if (phase) {
phase.status = 'completed';
phase.endTimestamp = new Date().toISOString();
// Calculer la durée
if (phase.timestamp) {
phase.duration = new Date(phase.endTimestamp) - new Date(phase.timestamp);
}
}
this.updateActivity();
}
}
// Capturer métriques des phases
if (str.includes('📊') && (str.includes('éléments') || str.includes('modifications') || str.includes('améliorés'))) {
// Capturer nombre d'éléments traités
const elementsMatch = str.match(/(\d+)\s+éléments/);
const modificationsMatch = str.match(/(\d+)\s+modifications?/);
const amelioresMatch = str.match(/(\d+)\/(\d+)\s+améliorés/);
// Associer à la dernière phase en cours
const currentPhase = this.phases.find(p => p.status === 'started') || this.phases[this.phases.length - 1];
if (currentPhase) {
if (elementsMatch) {
currentPhase.metrics.totalElements = parseInt(elementsMatch[1]);
}
if (modificationsMatch) {
currentPhase.metrics.modifications = parseInt(modificationsMatch[1]);
}
if (amelioresMatch) {
currentPhase.metrics.processed = parseInt(amelioresMatch[1]);
currentPhase.metrics.total = parseInt(amelioresMatch[2]);
}
}
}
// Capturer les phases Human Touch spécifiquement
if (str.includes('👤 PHASE 4/4: Human Touch') || str.includes('Human Touch (Simulation Humaine)')) {
const phase = {
number: 4,
name: 'Human Touch (Simulation Humaine)',
timestamp: new Date().toISOString(),
testContext: this.currentTestName || 'unknown',
status: 'started'
};
this.phases.push(phase);
this.updateActivity();
}
// Capturer prompts LLM complets
if (str.includes('🔍 ===== PROMPT ENVOYÉ À')) {
const providerMatch = str.match(/PROMPT ENVOYÉ À (\w+) \(([^)]+)\)/);
if (providerMatch) {
this.currentLLMCall = {
provider: providerMatch[1],
model: providerMatch[2],
startTime: Date.now(),
prompt: '', // Sera rempli par les lignes suivantes
response: ''
};
this.capturingPrompt = true;
}
}
// Capturer le contenu du prompt depuis le JSON log
if (this.capturingPrompt && this.currentLLMCall && str.includes('"msg"')) {
try {
const logLine = JSON.parse(str.trim());
if (logLine.msg && !logLine.msg.includes('🔍') && !logLine.msg.includes('📤')) {
// C'est le contenu du prompt
this.currentLLMCall.prompt = logLine.msg;
this.capturingPrompt = false; // Un seul message contient tout le prompt
}
} catch (e) {
// Pas du JSON, ignorer
}
}
// Arrêter la capture du prompt quand on voit la requête LLM
if (str.includes('📤 LLM REQUEST') && this.capturingPrompt) {
this.capturingPrompt = false;
}
// Capturer réponses LLM complètes
if (str.includes('📥 LLM RESPONSE')) {
if (this.currentLLMCall) {
this.currentLLMCall.endTime = Date.now();
this.currentLLMCall.testContext = this.currentTestName || 'unknown';
this.capturingResponse = true;
}
}
// Capturer le contenu de la réponse depuis le JSON log
if (this.capturingResponse && this.currentLLMCall && str.includes('"msg"')) {
try {
const logLine = JSON.parse(str.trim());
if (logLine.msg && !logLine.msg.includes('📥') && !logLine.msg.includes('✅') &&
!logLine.msg.includes('OPENAI') && !logLine.msg.includes('GEMINI') && !logLine.msg.includes('CLAUDE')) {
// C'est probablement le contenu de la réponse
if (!this.currentLLMCall.response) {
this.currentLLMCall.response = logLine.msg;
}
}
} catch (e) {
// Pas du JSON, ignorer
}
}
// Finaliser la capture LLM quand on voit le message de fin
if ((str.includes('✅ OPENAI') || str.includes('✅ GEMINI') || str.includes('✅ CLAUDE')) && this.capturingResponse) {
this.capturingResponse = false;
// Nettoyer les réponses
if (this.currentLLMCall.prompt) {
this.currentLLMCall.prompt = this.currentLLMCall.prompt.trim();
}
if (this.currentLLMCall.response) {
this.currentLLMCall.response = this.currentLLMCall.response.trim();
}
}
return originalWrite.call(process.stdout, chunk, encoding, callback);
};
// Also hook stderr in case Node.js test runner outputs to stderr
process.stderr.write = (chunk, encoding, callback) => {
const str = chunk.toString();
processTestLine(str);
return originalError.call(process.stderr, chunk, encoding, callback);
};
}
onTestStart(testName) {
this.currentTestName = testName;
this.testStartTime = Date.now();
this.updateActivity();
}
setTestContext(testName) {
this.currentTestName = testName;
this.updateActivity();
}
generateReport() {
const totalDuration = Date.now() - this.globalStartTime;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportFile = path.join(this.reportDir, `auto-report-${timestamp}.html`);
// Recalculer le nombre de LLM calls par test avec logique améliorée
this.testResults.forEach(test => {
test.llmCallsCount = this.llmCalls.filter(call => {
if (!call.testContext) return false;
// Recherche exacte ou inclusion
return call.testContext === test.name ||
call.testContext.includes(test.name) ||
test.name.includes(call.testContext) ||
(call.testContext !== 'unknown' && test.name.toLowerCase().includes('pipeline'));
}).length;
});
const stats = {
total: this.testResults.length,
passed: this.testResults.filter(r => r.status === 'passed').length,
failed: this.testResults.filter(r => r.status === 'failed').length,
totalDuration,
totalLLMCalls: this.llmCalls.length
};
const html = this.generateHTML(stats);
fs.writeFileSync(reportFile, html);
const jsonFile = path.join(this.reportDir, `auto-report-${timestamp}.json`);
fs.writeFileSync(jsonFile, JSON.stringify({
timestamp: new Date().toISOString(),
stats,
testResults: this.testResults,
llmCalls: this.llmCalls,
phases: this.phases // Ajout des phases dans le JSON
}, null, 2));
console.log(`\n📊 RAPPORT AUTO-GÉNÉRÉ:`);
console.log(` HTML: ${reportFile}`);
console.log(` JSON: ${jsonFile}`);
console.log(` 📈 ${stats.passed}/${stats.total} tests | ${stats.totalLLMCalls} LLM calls | ${Math.round(totalDuration/1000)}s`);
return reportFile;
}
generateHTML(stats) {
return `<!DOCTYPE html>
<html>
<head>
<title>Auto-Rapport TI - ${new Date().toLocaleString()}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 1000px; 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(150px, 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: 1.8em; font-weight: bold; color: #2196F3; }
.stat-label { color: #666; margin-top: 5px; font-size: 14px; }
.test-list { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.test-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: background-color 0.2s; }
.test-item:hover { background: #f8f9fa; }
.test-item.clickable { border-left: 4px solid #4CAF50; }
.test-status { width: 12px; height: 12px; border-radius: 50%; margin-right: 10px; }
.status-passed { background: #4CAF50; }
.status-failed { background: #f44336; }
.llm-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px; }
.llm-call { background: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 4px; display: flex; justify-content: space-between; cursor: pointer; transition: background-color 0.2s; }
.llm-call:hover { background: #e8f4f8; }
.llm-call.clickable { border-left: 4px solid #2196F3; }
.phases-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px; }
.phase-timeline { display: flex; flex-direction: column; gap: 10px; }
.phase-item { background: #f8f9fa; padding: 12px; border-radius: 6px; border-left: 4px solid #9C27B0; display: flex; justify-content: space-between; align-items: center; }
.phase-item.completed { border-left-color: #4CAF50; background: #f1f8e9; }
.phase-item.started { border-left-color: #FF9800; background: #fff3e0; }
.phase-number { width: 30px; height: 30px; border-radius: 50%; background: #9C27B0; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 12px; }
.phase-number.completed { background: #4CAF50; }
.phase-number.started { background: #FF9800; }
/* Modal styles */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
#llmModal { z-index: 1100; } /* LLM modal au-dessus du test modal */
.modal-content { background-color: white; margin: 5% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 800px; max-height: 80vh; overflow-y: auto; }
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.close:hover { color: black; }
.prompt-section, .response-section { margin: 15px 0; }
.prompt-section h4, .response-section h4 { color: #333; border-bottom: 2px solid #2196F3; padding-bottom: 5px; }
.prompt-content, .response-content { background: #f5f5f5; padding: 15px; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 14px; max-height: 300px; overflow-y: auto; }
.response-content { background: #f0f8ff; }
/* Test details modal styles */
.test-details-section { margin: 15px 0; }
.test-details-section h4 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 5px; }
.test-llm-list { background: #f8f9fa; padding: 10px; border-radius: 4px; margin-top: 10px; }
.test-llm-item { background: white; margin: 5px 0; padding: 8px; border-radius: 3px; display: flex; justify-content: space-between; cursor: pointer; border: 1px solid #ddd; }
.test-llm-item:hover { background: #e8f4f8; }
.test-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin: 10px 0; }
.test-stat { background: #f0f0f0; padding: 8px; border-radius: 4px; text-align: center; }
.test-stat-value { font-weight: bold; color: #2196F3; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Auto-Rapport Tests d'Intégration</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</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #4CAF50">${stats.passed}</div>
<div class="stat-label">Réussis</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #f44336">${stats.failed}</div>
<div class="stat-label">Échoués</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.totalLLMCalls}</div>
<div class="stat-label">LLM Calls</div>
</div>
<div class="stat-card">
<div class="stat-value">${Math.round(stats.totalDuration/1000)}s</div>
<div class="stat-label">Durée</div>
</div>
</div>
<div class="test-list">
<h3>Résultats des Tests</h3>
${this.testResults.map((test, index) => {
const testLLMCalls = this.llmCalls.filter(call => call.testContext === test.name);
const totalDuration = testLLMCalls.reduce((sum, call) => sum + call.duration, 0);
return `
<div class="test-item clickable" onclick="openTestModal(${index})">
<div style="display: flex; align-items: center;">
<span class="test-status status-${test.status}"></span>
<div>
<div><strong>${test.name}</strong></div>
<div style="font-size: 12px; color: #666;">
${test.llmCallsCount} LLM calls • ${Math.round(totalDuration/1000)}s durée LLM
</div>
</div>
</div>
<div style="text-align: right;">
<div style="color: ${test.status === 'passed' ? '#4CAF50' : '#f44336'}; font-weight: bold;">
${test.status === 'passed' ? '✓ RÉUSSI' : '✗ ÉCHOUÉ'}
</div>
<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">
🔍 Cliquer pour détails
</div>
</div>
</div>
`;
}).join('')}
</div>
${this.phases.length > 0 ? `
<div class="phases-section">
<h3>🔄 Pipeline Phases (${this.phases.length})</h3>
<div class="phase-timeline">
${this.phases.map((phase, index) => `
<div class="phase-item ${phase.status} clickable" onclick="openPhaseModal(${index})">
<div style="display: flex; align-items: center; flex: 1;">
<div class="phase-number ${phase.status}">${phase.number}</div>
<div style="flex: 1;">
<div><strong>Phase ${phase.number}/4: ${phase.name}</strong></div>
<div style="font-size: 12px; color: #666; margin-top: 2px;">
${phase.status === 'completed' ? '✅ Terminé' : '🔄 En cours'}${new Date(phase.timestamp).toLocaleTimeString()}
${phase.endTimestamp ? `${new Date(phase.endTimestamp).toLocaleTimeString()}` : ''}
${phase.duration ? ` (${Math.round(phase.duration)}ms)` : ''}
</div>
${Object.keys(phase.metrics || {}).length > 0 ? `
<div style="font-size: 11px; color: #888; margin-top: 2px;">
${phase.metrics.totalElements ? `📊 ${phase.metrics.totalElements} éléments` : ''}
${phase.metrics.modifications ? ` • ✏️ ${phase.metrics.modifications} modifs` : ''}
${phase.metrics.processed && phase.metrics.total ? ` • 🎯 ${phase.metrics.processed}/${phase.metrics.total} traités` : ''}
</div>
` : ''}
</div>
</div>
<div style="text-align: right;">
<div style="color: ${phase.status === 'completed' ? '#4CAF50' : '#FF9800'}; font-weight: bold;">
${phase.status === 'completed' ? '✓ COMPLÉTÉ' : '⏳ EN COURS'}
</div>
<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">
🔍 Cliquer pour détails
</div>
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="llm-section">
<h3>Appels LLM (${this.llmCalls.length})</h3>
${this.llmCalls.map((call, index) => {
const hasContent = call.prompt || call.response;
return `
<div class="llm-call ${hasContent ? 'clickable' : ''}" ${hasContent ? `onclick="openModal(${index})"` : ''}>
<div>
<strong>${call.provider}</strong> (${call.model})
<div style="font-size: 12px; color: #666; margin-top: 2px;">
Test: ${call.testContext}
</div>
</div>
<div style="text-align: right;">
<div>${call.duration}ms</div>
<div style="font-size: 12px; color: #666;">${call.tokens?.prompt || 0}${call.tokens?.response || 0} tokens</div>
${hasContent ? '<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">📋 Cliquer pour voir détails</div>' : ''}
</div>
</div>
`;
}).join('')}
</div>
<!-- Modal pour afficher les détails LLM -->
<div id="llmModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2 id="modalTitle">Détails de l'appel LLM</h2>
<div class="prompt-section">
<h4>🔍 Prompt envoyé</h4>
<div id="promptContent" class="prompt-content">Aucun prompt capturé</div>
</div>
<div class="response-section">
<h4>📥 Réponse reçue</h4>
<div id="responseContent" class="response-content">Aucune réponse capturée</div>
</div>
</div>
</div>
<!-- Modal pour afficher les détails de test -->
<div id="testModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeTestModal()">&times;</span>
<h2 id="testModalTitle">Détails du test</h2>
<div class="test-details-section">
<h4>📊 Statistiques</h4>
<div id="testStats" class="test-stats">
<!-- Stats will be populated here -->
</div>
</div>
<div class="test-details-section">
<h4>🚀 Appels LLM associés</h4>
<div id="testLLMCalls" class="test-llm-list">
<!-- LLM calls will be listed here -->
</div>
</div>
</div>
</div>
<!-- Modal pour afficher les détails de phase -->
<div id="phaseModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closePhaseModal()">&times;</span>
<h2 id="phaseModalTitle">Détails de la phase</h2>
<div class="test-details-section">
<h4>📊 Métriques de la phase</h4>
<div id="phaseMetrics" class="test-stats">
<!-- Metrics will be populated here -->
</div>
</div>
<div class="test-details-section">
<h4>🚀 Appels LLM de cette phase</h4>
<div id="phaseLLMCalls" class="test-llm-list">
<!-- Phase LLM calls will be listed here -->
</div>
</div>
<div class="test-details-section">
<h4>⏱️ Timeline</h4>
<div id="phaseTimeline" style="background: #f8f9fa; padding: 10px; border-radius: 4px;">
<!-- Timeline will be populated here -->
</div>
</div>
</div>
</div>
</div>
<script>
const llmCalls = ${JSON.stringify(this.llmCalls)};
const testResults = ${JSON.stringify(this.testResults)};
const phases = ${JSON.stringify(this.phases)};
function openModal(index) {
const call = llmCalls[index];
if (!call) return;
const modal = document.getElementById('llmModal');
const title = document.getElementById('modalTitle');
const promptContent = document.getElementById('promptContent');
const responseContent = document.getElementById('responseContent');
title.textContent = \`\${call.provider.toUpperCase()} (\${call.model}) - \${call.testContext}\`;
promptContent.textContent = call.prompt || 'Aucun prompt capturé';
responseContent.textContent = call.response || 'Aucune réponse capturée';
modal.style.display = 'block';
}
function closeModal() {
document.getElementById('llmModal').style.display = 'none';
}
function openTestModal(index) {
const test = testResults[index];
if (!test) return;
const modal = document.getElementById('testModal');
const title = document.getElementById('testModalTitle');
const statsDiv = document.getElementById('testStats');
const llmCallsDiv = document.getElementById('testLLMCalls');
title.textContent = \`Test: \${test.name}\`;
// Get LLM calls for this test
const testLLMCalls = llmCalls.filter(call => call.testContext === test.name);
const totalDuration = testLLMCalls.reduce((sum, call) => sum + call.duration, 0);
const totalTokensIn = testLLMCalls.reduce((sum, call) => sum + (call.tokens?.prompt || 0), 0);
const totalTokensOut = testLLMCalls.reduce((sum, call) => sum + (call.tokens?.response || 0), 0);
// Populate stats
statsDiv.innerHTML = \`
<div class="test-stat">
<div class="test-stat-value">\${test.status === 'passed' ? '✓' : '✗'}</div>
<div>Statut</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${testLLMCalls.length}</div>
<div>LLM Calls</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${Math.round(totalDuration/1000)}s</div>
<div>Durée LLM</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${totalTokensIn}</div>
<div>Tokens Input</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${totalTokensOut}</div>
<div>Tokens Output</div>
</div>
\`;
// Populate LLM calls
if (testLLMCalls.length > 0) {
llmCallsDiv.innerHTML = testLLMCalls.map((call, callIndex) => {
const globalIndex = llmCalls.findIndex(c => c === call);
return \`
<div class="test-llm-item" onclick="openModal(\${globalIndex})">
<div>
<strong>\${call.provider.toUpperCase()}</strong> (\${call.model})
<div style="font-size: 11px; color: #666; margin-top: 2px;">
\${new Date(call.timestamp).toLocaleTimeString()}
</div>
</div>
<div style="text-align: right;">
<div>\${call.duration}ms</div>
<div style="font-size: 11px; color: #666;">
\${call.tokens?.prompt || 0}→\${call.tokens?.response || 0} tokens
</div>
</div>
</div>
\`;
}).join('');
} else {
llmCallsDiv.innerHTML = '<div style="color: #666; text-align: center; padding: 20px;">Aucun appel LLM pour ce test</div>';
}
modal.style.display = 'block';
}
function closeTestModal() {
document.getElementById('testModal').style.display = 'none';
}
function openPhaseModal(index) {
const phase = phases[index];
if (!phase) return;
const modal = document.getElementById('phaseModal');
const title = document.getElementById('phaseModalTitle');
const metricsDiv = document.getElementById('phaseMetrics');
const llmCallsDiv = document.getElementById('phaseLLMCalls');
const timelineDiv = document.getElementById('phaseTimeline');
title.textContent = \`Phase \${phase.number}/4: \${phase.name}\`;
// Populate metrics
const metrics = phase.metrics || {};
metricsDiv.innerHTML = \`
<div class="test-stat">
<div class="test-stat-value">\${phase.status === 'completed' ? '✓' : '⏳'}</div>
<div>Statut</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${metrics.totalElements || 0}</div>
<div>Éléments</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${metrics.modifications || 0}</div>
<div>Modifications</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${metrics.processed || 0}/\${metrics.total || 0}</div>
<div>Traités</div>
</div>
<div class="test-stat">
<div class="test-stat-value">\${phase.duration ? Math.round(phase.duration) + 'ms' : 'N/A'}</div>
<div>Durée</div>
</div>
\`;
// Get LLM calls for this phase (by timestamp range)
const phaseStart = new Date(phase.timestamp);
const phaseEnd = phase.endTimestamp ? new Date(phase.endTimestamp) : new Date();
const phaseLLMCalls = llmCalls.filter(call => {
const callTime = new Date(call.timestamp);
return callTime >= phaseStart && callTime <= phaseEnd;
});
// Populate LLM calls
if (phaseLLMCalls.length > 0) {
llmCallsDiv.innerHTML = phaseLLMCalls.map((call, callIndex) => {
const globalIndex = llmCalls.findIndex(c => c === call);
return \`
<div class="test-llm-item" onclick="openModal(\${globalIndex})">
<div>
<strong>\${call.provider.toUpperCase()}</strong> (\${call.model})
<div style="font-size: 11px; color: #666; margin-top: 2px;">
\${new Date(call.timestamp).toLocaleTimeString()}
</div>
</div>
<div style="text-align: right;">
<div>\${call.duration}ms</div>
<div style="font-size: 11px; color: #666;">
\${call.promptTokens || 0}→\${call.responseTokens || 0} tokens
</div>
</div>
</div>
\`;
}).join('');
} else {
llmCallsDiv.innerHTML = '<div style="color: #666; text-align: center; padding: 20px;">Aucun appel LLM pour cette phase</div>';
}
// Populate timeline
timelineDiv.innerHTML = \`
<div style="margin-bottom: 10px;">
<strong>🚀 Début:</strong> \${new Date(phase.timestamp).toLocaleTimeString()}
</div>
\${phase.endTimestamp ? \`
<div style="margin-bottom: 10px;">
<strong>✅ Fin:</strong> \${new Date(phase.endTimestamp).toLocaleTimeString()}
</div>
<div style="margin-bottom: 10px;">
<strong>⏱️ Durée:</strong> \${phase.duration ? Math.round(phase.duration) + 'ms' : 'N/A'}
</div>
\` : '<div style="color: #FF9800;"><strong>⏳ En cours...</strong></div>'}
<div>
<strong>🧪 Test:</strong> \${phase.testContext || 'unknown'}
</div>
\`;
modal.style.display = 'block';
}
function closePhaseModal() {
document.getElementById('phaseModal').style.display = 'none';
}
// Fermer modal en cliquant à l'extérieur
window.onclick = function(event) {
const llmModal = document.getElementById('llmModal');
const testModal = document.getElementById('testModal');
const phaseModal = document.getElementById('phaseModal');
if (event.target === llmModal) {
closeModal();
}
if (event.target === testModal) {
closeTestModal();
}
if (event.target === phaseModal) {
closePhaseModal();
}
}
// Fermer modal avec Escape
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
closeTestModal();
closePhaseModal();
}
});
</script>
</body>
</html>`;
}
}
export { AutoReporter };