sourcefinder/tests/unit/scoring/BasicScoringEngine.test.js
Alexis Trouvé a7bd6115b7
Some checks failed
SourceFinder CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
SourceFinder CI/CD Pipeline / Unit Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Integration Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Performance Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Code Coverage Report (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (16.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (18.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (20.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Regression Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Audit (push) Has been cancelled
SourceFinder CI/CD Pipeline / Notify Results (push) Has been cancelled
feat: Implémentation complète du système SourceFinder avec tests
- Architecture modulaire avec injection de dépendances
- Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation)
- Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités)
- API REST complète avec validation et rate limiting
- Repository JSON avec index mémoire et backup automatique
- Provider LLM modulaire pour génération de contenu
- Suite de tests complète (Jest) :
  * Tests unitaires pour sécurité et scoring
  * Tests d'intégration API end-to-end
  * Tests de sécurité avec simulation d'attaques
  * Tests de performance et charge
- Pipeline CI/CD avec GitHub Actions
- Logging structuré et monitoring
- Configuration ESLint et environnement de test

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:06:10 +08:00

392 lines
15 KiB
JavaScript

/**
* 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();
});
});
});