/** * Tests unitaires pour BasicScoringEngine * Test du système de scoring selon CDC - couverture 90% minimum */ const BasicScoringEngine = require('../../../src/implementations/scoring/BasicScoringEngine'); describe('BasicScoringEngine', () => { let scoringEngine; beforeEach(() => { scoringEngine = new BasicScoringEngine(); }); describe('Initialisation', () => { test('devrait initialiser avec la configuration CDC correcte', () => { expect(scoringEngine).toBeInstanceOf(BasicScoringEngine); expect(scoringEngine.weights.specificity).toBe(0.4); expect(scoringEngine.weights.freshness).toBe(0.3); expect(scoringEngine.weights.quality).toBe(0.2); expect(scoringEngine.weights.reuse).toBe(0.1); }); test('devrait avoir les calculateurs initialisés', () => { expect(scoringEngine.specificityCalculator).toBeDefined(); expect(scoringEngine.freshnessCalculator).toBeDefined(); expect(scoringEngine.qualityCalculator).toBeDefined(); expect(scoringEngine.reuseCalculator).toBeDefined(); }); test('devrait initialiser les statistiques à zéro', () => { expect(scoringEngine.stats.totalScored).toBe(0); expect(scoringEngine.stats.averageScore).toBe(0); expect(Object.keys(scoringEngine.stats.scoreDistribution)).toHaveLength(0); }); }); describe('Score d\'un article individuel', () => { test('devrait scorer article avec race exacte', async () => { const article = testHelpers.createValidArticle({ title: 'Guide du Berger Allemand - Race 352', content: 'Le Berger Allemand (race 352-1) est un chien de taille grande...', publishDate: new Date().toISOString(), // Article très récent sourceDomain: 'centrale-canine.fr' // Source premium }); const context = testHelpers.createSearchContext({ raceCode: '352-1' }); const result = await scoringEngine.scoreArticle(article, context); expect(result.finalScore).toBeGreaterThan(80); // Devrait être excellent expect(result.specificityScore).toBeGreaterThan(90); // Race exacte expect(result.freshnessScore).toBeGreaterThan(90); // Très récent expect(result.qualityScore).toBeGreaterThan(80); // Source premium expect(result.scoreCategory).toBe('excellent'); expect(result.usageRecommendation).toBe('priority_use'); }); test('devrait pénaliser article générique', async () => { const article = testHelpers.createValidArticle({ title: 'Conseils généraux pour propriétaires de chiens', content: 'Tous les chiens ont besoin d\'exercice et d\'attention...', publishDate: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000).toISOString(), // 180 jours sourceDomain: 'blog-amateur.com' }); const context = testHelpers.createSearchContext({ raceCode: '352-1' }); const result = await scoringEngine.scoreArticle(article, context); expect(result.finalScore).toBeLessThan(50); // Devrait être faible expect(result.specificityScore).toBeLessThan(30); // Contenu générique expect(result.freshnessScore).toBeLessThan(20); // Article ancien expect(result.qualityScore).toBeLessThan(30); // Source amateur expect(result.scoreCategory).toBe('poor'); }); test('devrait calculer breakdown détaillé des scores', async () => { const article = testHelpers.createValidArticle(); const context = testHelpers.createSearchContext(); const result = await scoringEngine.scoreArticle(article, context); expect(result.scoringDetails).toBeDefined(); expect(result.scoringDetails.specificity).toHaveProperty('score'); expect(result.scoringDetails.specificity).toHaveProperty('reason'); expect(result.scoringDetails.freshness).toHaveProperty('score'); expect(result.scoringDetails.quality).toHaveProperty('score'); expect(result.scoringDetails.reuse).toHaveProperty('score'); }); test('devrait inclure métadonnées de scoring', async () => { const article = testHelpers.createValidArticle(); const context = testHelpers.createSearchContext(); const result = await scoringEngine.scoreArticle(article, context); expect(result.scoringMetadata.engine).toBe('BasicScoringEngine'); expect(result.scoringMetadata.version).toBe('1.0'); expect(result.scoringMetadata.weights).toEqual(scoringEngine.weights); expect(result.scoringMetadata.calculationTime).toBeGreaterThan(0); expect(result.scoringMetadata.scoredAt).toBeDefined(); }); test('devrait gérer les erreurs gracieusement', async () => { const invalidArticle = null; const context = testHelpers.createSearchContext(); const result = await scoringEngine.scoreArticle(invalidArticle, context); expect(result.finalScore).toBe(0); expect(result.scoreCategory).toBe('error'); expect(result.usageRecommendation).toBe('avoid'); expect(result.scoringDetails.error).toBeDefined(); }); }); describe('Scoring en lot (batch)', () => { test('devrait scorer plusieurs articles en parallèle', async () => { const articles = [ testHelpers.createValidArticle({ title: 'Article 1 sur Berger Allemand' }), testHelpers.createValidArticle({ title: 'Article 2 générique sur chiens' }), testHelpers.createValidArticle({ title: 'Article 3 spécialisé race 352' }) ]; const context = testHelpers.createSearchContext(); const results = await scoringEngine.batchScore(articles, context); expect(results).toHaveLength(3); expect(results[0].finalScore).toBeDefined(); expect(results[1].finalScore).toBeDefined(); expect(results[2].finalScore).toBeDefined(); // Vérifier tri par score décroissant expect(results[0].finalScore).toBeGreaterThanOrEqual(results[1].finalScore); expect(results[1].finalScore).toBeGreaterThanOrEqual(results[2].finalScore); }); test('devrait traiter batch vide', async () => { const result = await scoringEngine.batchScore([], testHelpers.createSearchContext()); expect(result).toEqual([]); }); test('devrait traiter batch avec articles invalides', async () => { const articles = [ testHelpers.createValidArticle(), null, testHelpers.createValidArticle() ]; const context = testHelpers.createSearchContext(); const results = await scoringEngine.batchScore(articles, context); expect(results).toHaveLength(3); expect(results[1].finalScore).toBe(0); // Article null expect(results[1].scoreCategory).toBe('error'); }); test('devrait respecter la limite de concurrence', async () => { const articles = Array(25).fill().map((_, i) => testHelpers.createValidArticle({ title: `Article ${i}` }) ); const context = testHelpers.createSearchContext(); const startTime = Date.now(); const results = await scoringEngine.batchScore(articles, context); expect(results).toHaveLength(25); expect(Date.now() - startTime).toBeLessThan(10000); // Doit finir en moins de 10s }); }); describe('Catégorisation des scores', () => { test('devrait catégoriser scores correctement', () => { expect(scoringEngine.categorizeScore(90)).toBe('excellent'); expect(scoringEngine.categorizeScore(75)).toBe('good'); expect(scoringEngine.categorizeScore(55)).toBe('fair'); expect(scoringEngine.categorizeScore(35)).toBe('poor'); expect(scoringEngine.categorizeScore(15)).toBe('reject'); }); }); describe('Recommandations d\'usage', () => { test('devrait recommander priority_use pour excellent score', () => { const recommendation = scoringEngine.generateUsageRecommendation( 90, // finalScore { score: 95 }, // specificity { score: 85 }, // freshness { score: 90 }, // quality { score: 80 } // reuse ); expect(recommendation).toBe('priority_use'); }); test('devrait recommander avoid pour score très faible', () => { const recommendation = scoringEngine.generateUsageRecommendation( 20, // finalScore { score: 10 }, // specificity { score: 30 }, // freshness { score: 20 }, // quality { score: 20 } // reuse ); expect(recommendation).toBe('avoid'); }); test('devrait recommander conditional_use pour score moyen avec qualité', () => { const recommendation = scoringEngine.generateUsageRecommendation( 55, // finalScore { score: 50 }, // specificity { score: 85 }, // freshness { score: 75 }, // quality { score: 60 } // reuse ); expect(recommendation).toBe('conditional_use'); }); }); describe('Explication des scores', () => { test('devrait expliquer score d\'article complet', async () => { const article = testHelpers.createValidArticle(); const context = testHelpers.createSearchContext(); const scoredArticle = await scoringEngine.scoreArticle(article, context); const explanation = scoringEngine.explainScore(scoredArticle); expect(explanation.scoreBreakdown).toBeDefined(); expect(explanation.scoreBreakdown.finalScore).toBe(scoredArticle.finalScore); expect(explanation.scoreBreakdown.components.specificity.contribution).toBeDefined(); expect(explanation.strengths).toBeInstanceOf(Array); expect(explanation.weaknesses).toBeInstanceOf(Array); expect(explanation.improvementSuggestions).toBeInstanceOf(Array); expect(explanation.usageGuideline.confidence).toMatch(/high|medium|low/); }); test('devrait gérer article sans détails de scoring', () => { const articleWithoutDetails = { finalScore: 50 }; const explanation = scoringEngine.explainScore(articleWithoutDetails); expect(explanation.error).toBeDefined(); expect(explanation.suggestion).toContain('Recalculer le score'); }); test('devrait identifier points forts et faibles', () => { const highScoreArticle = { specificityScore: 95, freshnessScore: 85, qualityScore: 90, reuseScore: 75, scoringDetails: {} }; const strengths = scoringEngine.identifyStrengths(highScoreArticle); expect(strengths).toContain('Excellente spécificité race'); expect(strengths).toContain('Source de haute qualité'); const lowScoreArticle = { specificityScore: 20, freshnessScore: 15, qualityScore: 25, reuseScore: 10, scoringDetails: {} }; const weaknesses = scoringEngine.identifyWeaknesses(lowScoreArticle); expect(weaknesses).toContain('Spécificité race insuffisante'); expect(weaknesses).toContain('Contenu trop ancien'); }); }); describe('Statistiques et métriques', () => { test('devrait mettre à jour statistiques après scoring', async () => { const article = testHelpers.createValidArticle(); const context = testHelpers.createSearchContext(); await scoringEngine.scoreArticle(article, context); const stats = scoringEngine.getStats(); expect(stats.totalScored).toBe(1); expect(stats.averageScore).toBeGreaterThan(0); expect(stats.calculationTime.average).toBeGreaterThan(0); }); test('devrait calculer distribution des scores', async () => { const articles = [ testHelpers.createValidArticle({ title: 'Excellent article spécialisé' }), testHelpers.createValidArticle({ title: 'Article moyen' }), testHelpers.createValidArticle({ title: 'Article générique' }) ]; const context = testHelpers.createSearchContext(); for (const article of articles) { await scoringEngine.scoreArticle(article, context); } const stats = scoringEngine.getStats(); expect(stats.totalScored).toBe(3); expect(Object.keys(stats.scoreDistribution).length).toBeGreaterThan(0); }); test('devrait réinitialiser statistiques', async () => { const article = testHelpers.createValidArticle(); const context = testHelpers.createSearchContext(); await scoringEngine.scoreArticle(article, context); expect(scoringEngine.getStats().totalScored).toBe(1); scoringEngine.resetStats(); expect(scoringEngine.getStats().totalScored).toBe(0); }); test('devrait calculer variance pour confiance', () => { const scores = [80, 85, 75, 90, 70]; const variance = scoringEngine.calculateVariance(scores); expect(variance).toBeGreaterThan(0); expect(variance).toBeLessThan(1000); // Variance raisonnable }); test('devrait calculer confiance basée sur homogénéité', () => { // Scores homogènes = confiance élevée const homogeneousArticle = { specificityScore: 85, freshnessScore: 80, qualityScore: 85, reuseScore: 80 }; const highConfidence = scoringEngine.calculateConfidence(homogeneousArticle); expect(highConfidence).toBe('high'); // Scores disparates = confiance faible const disparateArticle = { specificityScore: 90, freshnessScore: 20, qualityScore: 85, reuseScore: 10 }; const lowConfidence = scoringEngine.calculateConfidence(disparateArticle); expect(lowConfidence).toBe('low'); }); }); describe('Gestion des erreurs et edge cases', () => { test('devrait gérer articles sans dates', async () => { const article = testHelpers.createValidArticle({ publishDate: null, createdAt: null }); const context = testHelpers.createSearchContext(); const result = await scoringEngine.scoreArticle(article, context); expect(result.finalScore).toBeGreaterThanOrEqual(0); expect(result.freshnessScore).toBeDefined(); }); test('devrait gérer contexte incomplet', async () => { const article = testHelpers.createValidArticle(); const incompleteContext = { raceCode: null }; const result = await scoringEngine.scoreArticle(article, incompleteContext); expect(result.finalScore).toBeGreaterThanOrEqual(0); expect(result.scoreCategory).toBeDefined(); }); test('devrait maintenir performance avec grandes données', async () => { const largeArticle = testHelpers.createValidArticle({ content: 'x'.repeat(50000) // 50KB de contenu }); const context = testHelpers.createSearchContext(); const startTime = Date.now(); const result = await scoringEngine.scoreArticle(largeArticle, context); expect(Date.now() - startTime).toBeLessThan(1000); // Moins de 1 seconde expect(result.finalScore).toBeDefined(); }); }); });