/** * 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 ` Auto-Rapport TI - ${new Date().toLocaleString()}

📊 Auto-Rapport Tests d'Intégration

Généré automatiquement le ${new Date().toLocaleString()}

${stats.total}
Tests
${stats.passed}
Réussis
${stats.failed}
Échoués
${stats.totalLLMCalls}
LLM Calls
${Math.round(stats.totalDuration/1000)}s
Durée

Résultats des Tests

${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 `
${test.name}
${test.llmCallsCount} LLM calls • ${Math.round(totalDuration/1000)}s durée LLM
${test.status === 'passed' ? '✓ RÉUSSI' : '✗ ÉCHOUÉ'}
🔍 Cliquer pour détails
`; }).join('')}
${this.phases.length > 0 ? `

🔄 Pipeline Phases (${this.phases.length})

${this.phases.map((phase, index) => `
${phase.number}
Phase ${phase.number}/4: ${phase.name}
${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)` : ''}
${Object.keys(phase.metrics || {}).length > 0 ? `
${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` : ''}
` : ''}
${phase.status === 'completed' ? '✓ COMPLÉTÉ' : '⏳ EN COURS'}
🔍 Cliquer pour détails
`).join('')}
` : ''}

Appels LLM (${this.llmCalls.length})

${this.llmCalls.map((call, index) => { const hasContent = call.prompt || call.response; return `
${call.provider} (${call.model})
Test: ${call.testContext}
${call.duration}ms
${call.tokens?.prompt || 0}→${call.tokens?.response || 0} tokens
${hasContent ? '
📋 Cliquer pour voir détails
' : ''}
`; }).join('')}
`; } } export { AutoReporter };