"""Tests Sprint 88 — A.I.8 vue HTML : déficit projeté de robustesse. Couvre : 1. ``build_robustness_projection_html`` : - vide / None → ``""`` - rendu complet (résumé + détail) - calcul automatique de ``aggregated`` si non fourni - tri par déficit décroissant - colonne « pire dégradation » formatée - cellules colorées selon l'amplitude du déficit 2. Anti-injection sur nom de moteur + type de dégradation. 3. Bout-en-bout : intégration avec ``project_robustness_on_corpus`` + ``aggregate_projection_per_engine``. 4. Complétude i18n FR/EN. """ from __future__ import annotations import json from pathlib import Path from picarones.evaluation.metrics.robustness_projection import ( aggregate_projection_per_engine, project_robustness_on_corpus, ) from picarones.reports.html.renderers.robustness_projection import ( build_robustness_projection_html, ) def _load_labels(lang: str) -> dict: p = ( Path(__file__).parent.parent.parent / "picarones" / "reports" / "i18n" / f"{lang}.json" ) return json.loads(p.read_text(encoding="utf-8")) def _curve(engine: str, deg: str) -> dict: return { "engine_name": engine, "degradation_type": deg, "levels": [0, 5, 10, 20], "cer_values": [0.05, 0.10, 0.20, 0.50], "critical_threshold_level": 10, "cer_threshold": 0.20, } # ────────────────────────────────────────────────────────────────────────── # 1. build_robustness_projection_html # ────────────────────────────────────────────────────────────────────────── class TestRender: def test_none_returns_empty(self) -> None: assert build_robustness_projection_html(None) == "" def test_empty_returns_empty(self) -> None: assert build_robustness_projection_html({}) == "" def test_renders_summary_and_detail(self) -> None: projection = { "tess": { "noise": { "n_docs": 50, "n_docs_with_data": 48, "expected_cer_mean": 0.18, "baseline_cer": 0.05, "deficit_vs_baseline": 0.13, "n_docs_above_critical": 12, "critical_threshold_cer": 0.20, }, }, } labels = _load_labels("fr") html = build_robustness_projection_html(projection, labels=labels) assert " None: # Ne fournit que projection → aggregated calculé depuis projection = { "tess": { "noise": { "n_docs": 10, "n_docs_with_data": 10, "deficit_vs_baseline": 0.05, "n_docs_above_critical": 0, }, }, } html = build_robustness_projection_html( projection, labels=_load_labels("fr"), ) # Total = 0.05 = 5.00 points assert "+5.00" in html def test_sorted_by_deficit_descending(self) -> None: projection = { "low": { "noise": { "n_docs": 1, "n_docs_with_data": 1, "deficit_vs_baseline": 0.01, "n_docs_above_critical": 0, }, }, "high": { "noise": { "n_docs": 1, "n_docs_with_data": 1, "deficit_vs_baseline": 0.10, "n_docs_above_critical": 1, }, }, } html = build_robustness_projection_html( projection, labels=_load_labels("fr"), ) # « high » apparaît avant « low » dans le résumé assert html.index("high") < html.index("low") def test_anti_injection_engine(self) -> None: projection = { "": { "noise": { "n_docs": 1, "n_docs_with_data": 1, "deficit_vs_baseline": 0.05, "n_docs_above_critical": 0, }, }, } html = build_robustness_projection_html( projection, labels=_load_labels("fr"), ) assert "