Spaces:
Running
Running
Claude
feat(sprint-S8): cohรฉrence finale โ renames test dirs, /metrics endpoint, SBOM workflow
43478ec unverified | """Tests Sprint 62 โ vue HTML ยซ Profil philologique ยป. | |
| Couvre : | |
| 1. Sections individuelles : rendu correct quand au moins un moteur a | |
| du signal pour le module donnรฉ ; chaรฎne vide si aucun. | |
| 2. Agrรฉgateur : 6 sections prรฉsentes si les 6 modules ont du signal, | |
| sinon seulement les sections avec signal. | |
| 3. Adaptive masking complet : aucun moteur n'a de signal โ ``""``. | |
| 4. Anti-injection HTML : noms de moteurs / catรฉgories / caractรจres | |
| contenant ``<script>`` correctement รฉchappรฉs. | |
| 5. Cellules : code couleur appliquรฉ, valeurs en %. | |
| 6. Pas de classification automatique (le mot | |
| ยซ diplomatique ยป / ยซ modernisant ยป apparaรฎt seulement dans la | |
| note explicative, jamais comme รฉtiquette de moteur). | |
| 7. Intรฉgration dans le rapport HTML complet (FR + EN). | |
| 8. Complรฉtude i18n : toutes les clรฉs ``philo_*`` prรฉsentes en FR et EN. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from picarones.reports.html.renderers.philological import ( | |
| build_abbreviations_section, | |
| build_early_modern_section, | |
| build_modern_archives_section, | |
| build_mufi_section, | |
| build_philological_profile_html, | |
| build_roman_numerals_section, | |
| build_unicode_blocks_section, | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Fixtures | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _eng_with_unicode(name: str = "Tesseract", acc: float = 0.85) -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "unicode_blocks": { | |
| "global_accuracy": acc, "n_chars_total": 1000, | |
| "per_block": { | |
| "Latin Extended-A": { | |
| "correct": int(acc * 100), "total": 100, | |
| "accuracy": acc, | |
| }, | |
| "Alphabetic Presentation Forms": { | |
| "correct": 5, "total": 10, "accuracy": 0.5, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| def _eng_with_mufi(name: str = "Pero", coverage: float = 0.78) -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "mufi": {"coverage": coverage, "n_mufi_chars_reference": 100}, | |
| }, | |
| } | |
| def _eng_with_abbreviations(name: str = "T", strict: float = 0.6) -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "abbreviations": { | |
| "global_strict_score": strict, | |
| "global_expansion_score": 0.95, | |
| "n_abbreviations_in_reference": 50, | |
| }, | |
| }, | |
| } | |
| def _eng_with_early_modern(name: str = "T") -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "early_modern": { | |
| "n_markers_reference": 100, | |
| "n_markers_preserved": 70, | |
| "global_preservation": 0.7, | |
| "per_category": { | |
| "ligatures": {"total": 30, "preserved": 25, "preservation": 25 / 30}, | |
| "long_s": {"total": 50, "preserved": 30, "preservation": 0.6}, | |
| "ampersand": {"total": 20, "preserved": 15, "preservation": 0.75}, | |
| }, | |
| }, | |
| }, | |
| } | |
| def _eng_with_modern_archives(name: str = "T") -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "modern_archives": { | |
| "n_markers_reference": 100, | |
| "n_strict_preserved": 60, | |
| "n_expansion_preserved": 90, | |
| "global_strict_score": 0.6, | |
| "global_expansion_score": 0.9, | |
| "per_category": { | |
| "civility_titles": { | |
| "n_total": 30, "n_strict_preserved": 25, | |
| "n_expansion_preserved": 28, | |
| "strict_score": 25 / 30, "expansion_score": 28 / 30, | |
| }, | |
| "address": { | |
| "n_total": 20, "n_strict_preserved": 10, | |
| "n_expansion_preserved": 18, | |
| "strict_score": 0.5, "expansion_score": 0.9, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| def _eng_with_roman(name: str = "T") -> dict: | |
| return { | |
| "name": name, | |
| "aggregated_philological": { | |
| "roman_numerals": { | |
| "n_numerals_reference": 20, | |
| "per_status": { | |
| "strict_preserved": 12, "case_changed": 3, | |
| "j_dropped": 2, "converted_to_arabic": 2, "lost": 1, | |
| }, | |
| }, | |
| }, | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 1. Sections individuelles | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestIndividualSections: | |
| def test_unicode_blocks_renders(self) -> None: | |
| html = build_unicode_blocks_section([_eng_with_unicode()]) | |
| assert "Prรฉcision par bloc Unicode" in html | |
| assert "Tesseract" in html | |
| assert "Latin Extended-A" in html | |
| def test_unicode_blocks_empty_without_signal(self) -> None: | |
| html = build_unicode_blocks_section([_eng_with_mufi()]) | |
| assert html == "" | |
| def test_abbreviations_renders(self) -> None: | |
| html = build_abbreviations_section([_eng_with_abbreviations()]) | |
| assert "Abrรฉviations mรฉdiรฉvales" in html | |
| assert "T" in html | |
| def test_mufi_renders(self) -> None: | |
| html = build_mufi_section([_eng_with_mufi()]) | |
| assert "Couverture MUFI" in html | |
| assert "Pero" in html | |
| def test_early_modern_renders(self) -> None: | |
| html = build_early_modern_section([_eng_with_early_modern()]) | |
| assert "Marqueurs typographiques" in html | |
| assert "ligatures" in html | |
| assert "long_s" in html | |
| assert "ampersand" in html | |
| def test_modern_archives_renders(self) -> None: | |
| html = build_modern_archives_section([_eng_with_modern_archives()]) | |
| assert "Abrรฉviations des archives modernes" in html | |
| assert "civility_titles" in html | |
| assert "address" in html | |
| def test_roman_numerals_renders(self) -> None: | |
| html = build_roman_numerals_section([_eng_with_roman()]) | |
| assert "Numรฉraux romains" in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 2-3. Agrรฉgateur + adaptive masking | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestAggregator: | |
| def test_returns_empty_when_no_engine_has_signal(self) -> None: | |
| engines = [{"name": "X", "aggregated_philological": None}] | |
| assert build_philological_profile_html(engines) == "" | |
| def test_returns_empty_when_engines_summary_empty(self) -> None: | |
| assert build_philological_profile_html([]) == "" | |
| def test_includes_only_modules_with_signal(self) -> None: | |
| # Un seul moteur avec MUFI uniquement | |
| html = build_philological_profile_html([_eng_with_mufi()]) | |
| assert html != "" | |
| assert "Couverture MUFI" in html | |
| # Sections sans signal absentes | |
| assert "Prรฉcision par bloc Unicode" not in html | |
| assert "Abrรฉviations mรฉdiรฉvales" not in html | |
| assert "Marqueurs typographiques" not in html | |
| assert "Abrรฉviations des archives modernes" not in html | |
| assert "Numรฉraux romains" not in html | |
| def test_includes_all_six_when_full_signal(self) -> None: | |
| engines = [ | |
| _eng_with_unicode(), | |
| _eng_with_mufi(), | |
| _eng_with_abbreviations(), | |
| _eng_with_early_modern(), | |
| _eng_with_modern_archives(), | |
| _eng_with_roman(), | |
| ] | |
| html = build_philological_profile_html(engines) | |
| for marker in ( | |
| "Prรฉcision par bloc Unicode", | |
| "Abrรฉviations mรฉdiรฉvales", | |
| "Couverture MUFI", | |
| "Marqueurs typographiques", | |
| "Abrรฉviations des archives modernes", | |
| "Numรฉraux romains", | |
| ): | |
| assert marker in html, f"section absente : {marker}" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 4. Anti-injection HTML | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestAntiInjection: | |
| def test_engine_name_with_script_escaped(self) -> None: | |
| eng = _eng_with_mufi(name="<script>alert(1)</script>") | |
| html = build_mufi_section([eng]) | |
| assert "<script>" not in html | |
| assert "<script>" in html | |
| def test_section_title_safely_escaped_via_labels(self) -> None: | |
| labels = {"philo_mufi_title": "<b>Hack</b>"} | |
| html = build_mufi_section([_eng_with_mufi()], labels=labels) | |
| assert "<b>Hack</b>" not in html | |
| assert "<b>Hack</b>" in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 5. Cellules : couleur + valeur en % | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestCells: | |
| def test_score_displayed_in_percent(self) -> None: | |
| html = build_mufi_section([_eng_with_mufi(coverage=0.78)]) | |
| assert "78.0%" in html | |
| def test_color_present(self) -> None: | |
| # Le style background:#... doit apparaรฎtre | |
| html = build_mufi_section([_eng_with_mufi(coverage=0.5)]) | |
| assert "background:#" in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 6. Pas de classification imposรฉe | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestNoForcedClassification: | |
| def test_engine_not_labeled_as_diplomatic_or_modernizing(self) -> None: | |
| # Le moteur a strict=1.0 (typique diplomatique) mais on ne | |
| # doit pas voir ยซ diplomatique ยป comme รฉtiquette de cellule. | |
| eng = _eng_with_abbreviations(name="DiploEngine", strict=1.0) | |
| html = build_abbreviations_section([eng]) | |
| # ยซ DiploEngine ยป apparaรฎt parce que c'est le nom du moteur. | |
| assert "DiploEngine" in html | |
| # Le mot ยซ diplomatique ยป n'apparaรฎt que dans la note | |
| # explicative en bas (et peut รชtre absent par dรฉfaut). | |
| # On vรฉrifie qu'il n'est pas accolรฉ au nom du moteur dans | |
| # une cellule de tableau. | |
| assert "DiploEngine</td>diplomatique" not in html | |
| assert "DiploEngine</td>modernisant" not in html | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # 7. Complรฉtude i18n | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class TestI18nCompleteness: | |
| def _load(self, lang: str) -> dict: | |
| path = ( | |
| Path(__file__).parent.parent.parent | |
| / "picarones" / "reports" / "i18n" / f"{lang}.json" | |
| ) | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| def test_all_philo_keys_present_fr(self) -> None: | |
| d = self._load("fr") | |
| required = ( | |
| "philo_profile_title", "philo_profile_note", | |
| "philo_engine_label", "philo_global_label", | |
| "philo_strict_label", "philo_expansion_label", | |
| "philo_n_total_label", | |
| "philo_unicode_blocks_title", "philo_unicode_blocks_note", | |
| "philo_abbreviations_title", "philo_abbreviations_note", | |
| "philo_mufi_title", "philo_mufi_note", | |
| "philo_mufi_coverage_label", | |
| "philo_early_modern_title", "philo_early_modern_note", | |
| "philo_modern_archives_title", "philo_modern_archives_note", | |
| "philo_roman_numerals_title", "philo_roman_numerals_note", | |
| "philo_roman_status_strict_preserved", | |
| "philo_roman_status_case_changed", | |
| "philo_roman_status_j_dropped", | |
| "philo_roman_status_converted_to_arabic", | |
| "philo_roman_status_lost", | |
| ) | |
| for key in required: | |
| assert key in d, f"manque clรฉ FR : {key}" | |
| def test_all_philo_keys_present_en(self) -> None: | |
| d_fr = self._load("fr") | |
| d_en = self._load("en") | |
| for key in d_fr: | |
| if key.startswith("philo_"): | |
| assert key in d_en, f"manque clรฉ EN : {key}" | |