"""Tests unitaires pour la détection dénutrition HAS/FFN 2021.""" import re import pytest from src.config import BiologieCle, Diagnostic, DossierMedical, Sejour from src.medical.diagnostic_extraction import _detect_nutrition_has2021, _DAS_PATTERNS from src.medical.cim10_dict import normalize_text # ── Helpers ────────────────────────────────────────────────────────── def _make_dossier(age=None, imc=None, albumine=None, existing_codes=None): """Construit un DossierMedical minimal pour les tests.""" dossier = DossierMedical() dossier.sejour = Sejour(age=age, imc=imc) if albumine is not None: dossier.biologie_cle.append( BiologieCle( test="Albumine", valeur=str(albumine), valeur_num=float(albumine), anomalie=True, quality="ok", ) ) for code in (existing_codes or []): dossier.diagnostics_associes.append( Diagnostic(texte="existant", cim10_suggestion=code, source="test") ) return dossier # ── Tests _detect_nutrition_has2021 ────────────────────────────────── class TestDetectNutritionHAS2021: """Tests de la détection déterministe basée sur IMC/âge/albumine.""" def test_adulte_imc17_albumine28_gives_E43(self): """Adulte IMC 17.0 + albumine 28 → E43 (sévère via IMC ≤17 ET albumine <30).""" dossier = _make_dossier(age=50, imc=17.0, albumine=28) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E43" in codes def test_adulte_imc18_sans_albumine_gives_E44(self): """Adulte IMC 18.0 sans albumine → E44.0 (modéré, 17 < IMC < 18.5).""" dossier = _make_dossier(age=45, imc=18.0) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E44.0" in codes def test_personne_agee_75_imc21_gives_E44(self): """≥70 ans IMC 21.0 → E44.0 (seuil gériatrique < 22).""" dossier = _make_dossier(age=75, imc=21.0) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E44.0" in codes def test_personne_agee_75_imc19_gives_E43(self): """≥70 ans IMC 19.0 → E43 (sévère, IMC < 20).""" dossier = _make_dossier(age=75, imc=19.0) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E43" in codes def test_adulte_imc25_no_das(self): """Adulte IMC 25.0 → aucun DAS (au-dessus du seuil).""" dossier = _make_dossier(age=40, imc=25.0) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert not any(c in codes for c in ("E43", "E44.0", "E46")) def test_e46_deja_code_no_ajout(self): """E46 déjà codé → aucun ajout.""" dossier = _make_dossier(age=50, imc=16.0, existing_codes=["E46"]) _detect_nutrition_has2021(dossier) e_codes = [d.cim10_suggestion for d in dossier.diagnostics_associes if d.cim10_suggestion in ("E43", "E44.0", "E46")] # Seul le E46 existant doit être présent assert e_codes == ["E46"] def test_pas_imc_no_ajout(self): """Pas d'IMC → aucun ajout (dégradation gracieuse).""" dossier = _make_dossier(age=50, imc=None) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert not any(c in codes for c in ("E43", "E44.0", "E46")) def test_albumine_upgrade_severity(self): """IMC modéré + albumine < 30 → upgrade vers E43.""" dossier = _make_dossier(age=50, imc=18.0, albumine=25) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E43" in codes # Albumine < 30 → sévère def test_alerte_codage_added(self): """Vérifie qu'une alerte codage est ajoutée.""" dossier = _make_dossier(age=50, imc=17.0) _detect_nutrition_has2021(dossier) assert any("HAS 2021" in a for a in dossier.alertes_codage) def test_source_is_has2021(self): """Vérifie que la source est 'has2021'.""" dossier = _make_dossier(age=50, imc=17.0) _detect_nutrition_has2021(dossier) has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"] assert len(has_diags) == 1 def test_age_inconnu_seuils_adulte(self): """Âge inconnu → seuils adulte par défaut.""" dossier = _make_dossier(age=None, imc=17.0) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E43" in codes # IMC ≤ 17 → sévère (seuils adulte) def test_personne_agee_70_exact_seuil(self): """70 ans exactement → utilise les seuils gériatriques.""" dossier = _make_dossier(age=70, imc=21.5) _detect_nutrition_has2021(dossier) codes = [d.cim10_suggestion for d in dossier.diagnostics_associes] assert "E44.0" in codes # < 22 → modéré avec seuils ≥70 # ── Tests regex albumine (bio_extraction) ──────────────────────────── class TestAlbumineRegex: """Vérifie l'extraction regex de l'albumine.""" ALBUMINE_PATTERN = r"(?:[Aa]lbumin[ée]?(?:mie)?|[Aa]lb(?:u)?[ée]?(?:mie)?)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:g/[Ll])?" PREALBUMINE_PATTERN = r"(?:[Pp]r[ée]albumine|[Tt]ransthyr[ée]tine)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:mg/[Ll]|g/[Ll])?" def test_albumine_standard(self): m = re.search(self.ALBUMINE_PATTERN, "Albumine = 28 g/L") assert m and m.group(1) == "28" def test_albumine_colon(self): m = re.search(self.ALBUMINE_PATTERN, "albumine: 32.5 g/L") assert m and m.group(1) == "32.5" def test_albumine_sans_unite(self): m = re.search(self.ALBUMINE_PATTERN, "Albumine 28") assert m and m.group(1) == "28" def test_albuminemie(self): m = re.search(self.ALBUMINE_PATTERN, "Albuminémie à 25 g/L") assert m and m.group(1) == "25" def test_prealbumine(self): m = re.search(self.PREALBUMINE_PATTERN, "Préalbumine = 0.15 g/L") assert m and m.group(1) == "0.15" def test_transthyretine(self): m = re.search(self.PREALBUMINE_PATTERN, "Transthyrétine: 180 mg/L") assert m and m.group(1) == "180" # ── Tests regex texte dénutrition (DAS patterns) ──────────────────── class TestDenutritionRegexSeverity: """Vérifie que les patterns textuels de dénutrition détectent la sévérité.""" def _match_pattern(self, text): """Retourne le (label, code) du premier pattern DAS matché.""" text_norm = normalize_text(text.lower()) for pat, label, code in _DAS_PATTERNS: if re.search(pat, text_norm): return label, code return None, None def test_denutrition_severe_gives_E43(self): _, code = self._match_pattern("denutrition severe") assert code == "E43" def test_denutrition_moderee_gives_E44(self): _, code = self._match_pattern("denutrition moderee") assert code == "E44.0" def test_denutrition_generic_gives_E46(self): _, code = self._match_pattern("denutrition") assert code == "E46" def test_malnutrition_severe_gives_E43(self): _, code = self._match_pattern("malnutrition severe") assert code == "E43" def test_denutrition_grade_iii_gives_E43(self): _, code = self._match_pattern("denutrition grade III") assert code == "E43" def test_hypoalbuminemie_severe_gives_E46(self): """hypoalbuminemie severe → E46 (pattern générique).""" _, code = self._match_pattern("hypoalbuminemie severe") assert code == "E46"