/** * 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 `
Généré automatiquement le ${new Date().toLocaleString()}
${JSON.stringify(test.config, null, 2)}