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
- 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>
392 lines
15 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
}); |