"""Tests d'intégration : détection dénutrition HAS/FFN 2021 sur dossiers réalistes. 4 cas cliniques réalistes passés dans le pipeline complet extract_medical_info. Vérifie l'interaction entre regex bio, détection HAS 2021, conflits, et sévérité CMA. """ import pytest from src.config import DossierMedical from src.medical.cim10_extractor import extract_medical_info # ── Cas 1 : Personne âgée dénutrie (≥70 ans, seuils gériatriques) ─── class TestCas1PersonneAgeeDenutrie: """Mme D., 81 ans, hospitalisée pour pneumopathie. IMC 20.5 → sous le seuil gériatrique < 22 → dénutrition modérée. Albumine 27 g/L → < 30 → critère de sévérité → upgrade vers E43. Attendu : E43 (dénutrition sévère) détecté par HAS 2021. """ @pytest.fixture def dossier(self) -> DossierMedical: parsed = { "type": "crh", "patient": { "sexe": "F", "date_naissance": "15/03/1943", }, "sejour": { "date_entree": "10/01/2025", "date_sortie": "20/01/2025", }, "diagnostics": [], "signes_vitaux": {"imc": 20.5, "poids_kg": 48, "taille_cm": 153}, } text = """\ Votre patiente née le 15/03/1943 a été hospitalisée du 10/01/2025 au 20/01/2025. Antécédents : - Hypertension artérielle - Fibrillation auriculaire paroxystique - Arthrose invalidante Au total : Pneumopathie basale droite d'évolution favorable sous antibiothérapie. Biologie d'entrée : CRP = 145 mg/L Albumine = 27 g/L Créatinine = 95 µmol/L Sodium = 138 mmol/L Potassium = 4.1 mmol/L Hémoglobine = 10.2 g/dL IMC: 20.5 TTT de sortie : Amoxicilline 1g matin et soir pendant 5 jours Paracétamol si besoin Devenir : retour à domicile.""" return extract_medical_info(parsed, text) def test_age_81_ans(self, dossier): assert dossier.sejour.age == 81 def test_imc_extrait(self, dossier): assert dossier.sejour.imc == 20.5 def test_albumine_extraite(self, dossier): """L'albumine doit être extraite par le nouveau regex.""" alb = [b for b in dossier.biologie_cle if b.test == "Albumine"] assert len(alb) >= 1 assert alb[0].valeur_num == 27.0 def test_denutrition_severe_E43(self, dossier): """IMC 20.5 (modéré ≥70 ans) + albumine 27 (<30) → E43 sévère.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E43" in codes, f"E43 attendu, trouvé : {codes}" def test_source_has2021(self, dossier): has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"] assert len(has_diags) == 1 assert has_diags[0].cim10_suggestion == "E43" def test_alerte_has2021(self, dossier): assert any("HAS 2021" in a for a in dossier.alertes_codage) def test_pneumopathie_detectee(self, dossier): """Le DP/DAS pneumopathie ne doit pas être impacté.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} # Pneumopathie J18.9 ou DP all_codes = codes.copy() if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion: all_codes.add(dossier.diagnostic_principal.cim10_suggestion) assert "J18.9" in all_codes def test_hta_detectee(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "I10" in codes # ── Cas 2 : Patient obèse ET dénutri (conflit E66 + E43 = MEDIUM) ─── class TestCas2ObeseDenutri: """M. B., 58 ans, hospitalisé pour pancréatite aiguë sur obésité. IMC 35.2 → obèse (E66.0). Dénutrition sévère mentionnée dans le texte (E43). Le conflit E66/dénutrition doit être MEDIUM (pas HARD) selon HAS 2021. La coexistence est cliniquement possible (sarcopénie de l'obèse). DP fourni via Trackare (K85.9) pour éviter que NUKE-3 ne réorganise les codes. """ @pytest.fixture def dossier(self) -> DossierMedical: parsed = { "type": "trackare", "patient": { "sexe": "M", "date_naissance": "22/07/1967", }, "sejour": { "date_entree": "05/02/2025", "date_sortie": "12/02/2025", }, "diagnostics": [ { "type": "Principal", "statut": "actif", "code_cim10": "K85.9", "libelle": "Pancréatite aiguë", } ], "signes_vitaux": {"imc": 35.2, "poids_kg": 110, "taille_cm": 177}, } text = """\ Pancréatite aiguë sur terrain d'obésité morbide. Dénutrition sévère protéique avec sarcopénie documentée, perte de poids de 15 kg en 3 mois. Antécédents : - Diabète type 2 - Tabagisme actif Biologie : CRP = 145 mg/L Albumine = 25 g/L Lipasémie = 1200 UI/L HbA1c = 7.8 % Hémoglobine = 13.5 g/dL IMC: 35.2 TTT de sortie : Metformine 1000mg matin et soir Paracétamol si besoin Devenir : retour à domicile.""" return extract_medical_info(parsed, text) def test_dp_pancreatite(self, dossier): """DP = K85.9 (fourni par Trackare).""" assert dossier.diagnostic_principal is not None assert dossier.diagnostic_principal.cim10_suggestion == "K85.9" def test_obesite_E66_detectee(self, dossier): """L'obésité doit être détectée via IMC ≥ 30.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E66.0" in codes def test_denutrition_severe_regex_E43(self, dossier): """'Dénutrition sévère' dans le texte → E43 via regex.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E43" in codes, f"E43 attendu via regex 'denutrition severe', trouvé : {codes}" def test_coexistence_E66_E43(self, dossier): """E66.0 et E43 doivent coexister (pas de blocage HARD).""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E66.0" in codes and "E43" in codes def test_albumine_extraite(self, dossier): alb = [b for b in dossier.biologie_cle if b.test == "Albumine"] assert len(alb) >= 1 assert alb[0].valeur_num == 25.0 def test_diabete_detecte(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E11.9" in codes def test_tabagisme_detecte(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "F17.2" in codes # ── Cas 3 : Adulte dénutri modéré sans albumine (IMC seul) ────────── class TestCas3AdulteDenutriModere: """Mme L., 45 ans, hospitalisée pour angiocholite. IMC 17.8 → entre 17 et 18.5 → dénutrition modérée (E44.0) par HAS 2021. Pas d'albumine → pas d'upgrade de sévérité. """ @pytest.fixture def dossier(self) -> DossierMedical: parsed = { "type": "crh", "patient": { "sexe": "F", "date_naissance": "12/09/1979", }, "sejour": { "date_entree": "15/03/2025", "date_sortie": "21/03/2025", }, "diagnostics": [], "signes_vitaux": {"imc": 17.8, "poids_kg": 48, "taille_cm": 164}, } text = """\ Votre patiente née le 12/09/1979 a été hospitalisée du 15/03/2025 au 21/03/2025. Antécédents : - Lithiases vésiculaires - Anorexie restrictive ancienne (adolescence) Au total : Angiocholite sur lithiase du cholédoque traitée par CPRE puis cholécystectomie par cœlioscopie. Biologie d'entrée : CRP = 92 mg/L ASAT = 180 UI/L ALAT = 210 UI/L Bilirubine totale = 45 µmol/L Lipasémie = 890 UI/L Hémoglobine = 11.8 g/dL Leucocytes = 12.5 G/L IMC: 17.8 TTT de sortie : Paracétamol 1g x3/jour Spasfon si besoin Devenir : retour à domicile.""" return extract_medical_info(parsed, text) def test_imc_178(self, dossier): assert dossier.sejour.imc == 17.8 def test_denutrition_moderee_E44(self, dossier): """IMC 17.8 adulte → E44.0 (modéré, 17 < IMC < 18.5).""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E44.0" in codes, f"E44.0 attendu, trouvé : {codes}" def test_pas_E43(self, dossier): """Pas de sévère sans albumine basse ni IMC ≤ 17.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E43" not in codes def test_pas_albumine_extraite(self, dossier): """Pas d'albumine dans le texte → pas d'extraction.""" alb = [b for b in dossier.biologie_cle if b.test == "Albumine"] assert len(alb) == 0 def test_angiocholite_detectee(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} all_codes = codes.copy() if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion: all_codes.add(dossier.diagnostic_principal.cim10_suggestion) assert "K83.0" in all_codes def test_cholecystectomie_detectee(self, dossier): acte_codes = {a.code_ccam_suggestion for a in dossier.actes_ccam} assert "HMFC004" in acte_codes def test_alerte_has2021(self, dossier): assert any("HAS 2021" in a for a in dossier.alertes_codage) # ── Cas 4 : IMC normal, pas de dénutrition (contrôle négatif) ─────── class TestCas4ControleNegatif: """M. R., 55 ans, hospitalisé pour colique hépatique. IMC 26.3 → au-dessus de tous les seuils → aucune dénutrition. Albumine 38 g/L → normale. Vérifie que la détection HAS 2021 ne produit pas de faux positif. """ @pytest.fixture def dossier(self) -> DossierMedical: parsed = { "type": "crh", "patient": { "sexe": "M", "date_naissance": "30/11/1969", }, "sejour": { "date_entree": "01/04/2025", "date_sortie": "03/04/2025", }, "diagnostics": [], "signes_vitaux": {"imc": 26.3, "poids_kg": 82, "taille_cm": 176}, } text = """\ Votre patient né le 30/11/1969 a été hospitalisé du 01/04/2025 au 03/04/2025. Antécédents : - Hypertension artérielle - Dyslipidémie Au total : Colique hépatique sur lithiase vésiculaire. Bonne évolution. Cholécystectomie programmée à distance. Biologie : CRP = 12 mg/L Albumine = 38 g/L ASAT = 35 UI/L ALAT = 42 UI/L Hémoglobine = 14.5 g/dL Créatinine = 78 µmol/L IMC: 26.3 TTT de sortie : Paracétamol si besoin Spasfon si besoin Devenir : retour à domicile.""" return extract_medical_info(parsed, text) def test_imc_normal(self, dossier): assert dossier.sejour.imc == 26.3 def test_albumine_normale_extraite(self, dossier): """Albumine 38 g/L → extraite mais normale.""" alb = [b for b in dossier.biologie_cle if b.test == "Albumine"] assert len(alb) >= 1 assert alb[0].valeur_num == 38.0 def test_pas_de_denutrition(self, dossier): """IMC 26.3 + albumine 38 → aucun code E40-E46.""" codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} nutrition_codes = {c for c in codes if c and c.startswith("E4") and c[:3] in ("E40", "E41", "E42", "E43", "E44", "E45", "E46")} assert not nutrition_codes, f"Faux positif dénutrition : {nutrition_codes}" def test_pas_alerte_has2021(self, dossier): """Aucune alerte HAS 2021 ne doit apparaître.""" assert not any("HAS 2021" in a for a in dossier.alertes_codage) def test_hta_detectee(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "I10" in codes def test_dyslipidemie_detectee(self, dossier): codes = {d.cim10_suggestion for d in dossier.diagnostics_associes} assert "E78.5" in codes