"""Tests de la checklist de complétude documentaire DIM.""" import pytest from src.config import ( ActeCCAM, BiologieCle, CheckCompletude, CompletudeDossier, Diagnostic, DossierMedical, Imagerie, ItemCompletude, PreuveClinique, Sejour, load_completude_rules, ) from src.quality.completude import build_completude_checklist # ── Helpers ────────────────────────────────────────────────────────── def _make_dossier(**kwargs) -> DossierMedical: """Crée un DossierMedical minimal avec des valeurs par défaut.""" return DossierMedical( sejour=kwargs.get("sejour", Sejour()), diagnostic_principal=kwargs.get("dp", None), diagnostics_associes=kwargs.get("das", []), biologie_cle=kwargs.get("bio", []), imagerie=kwargs.get("imagerie", []), actes_ccam=kwargs.get("actes", []), document_type=kwargs.get("document_type", "crh"), source_files=kwargs.get("source_files", []), ) # ── Tests du chargement YAML ──────────────────────────────────────── class TestLoadRules: def test_load_completude_rules(self): rules = load_completude_rules() assert "diagnostics" in rules assert "actes" in rules assert len(rules["diagnostics"]) >= 10 # Au moins 10 familles def test_rules_structure(self): rules = load_completude_rules() for family_id, family in rules["diagnostics"].items(): assert "prefixes" in family, f"Famille {family_id} sans prefixes" assert "items" in family, f"Famille {family_id} sans items" for item in family["items"]: assert "categorie" in item assert "element" in item assert "importance" in item assert item["importance"] in ("obligatoire", "recommande") def test_yaml_version_2(self): """Le YAML enrichi doit être en version 2.""" rules = load_completude_rules() assert rules.get("version") == 2 def test_seuils_present_in_yaml(self): """Vérifier que les seuils sont bien chargés sur certains items.""" rules = load_completude_rules() denut = rules["diagnostics"]["denutrition"] items_with_seuil = [i for i in denut["items"] if "seuil" in i] assert len(items_with_seuil) >= 2, "Dénutrition doit avoir au moins 2 items avec seuil" # ── Tests dénutrition ──────────────────────────────────────────────── class TestDenutrition: def test_denutrition_complete(self): """E43 avec albumine + IMC → defendable, score élevé.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition sévère", cim10_suggestion="E43"), sejour=Sejour(imc=16.5), bio=[BiologieCle(test="Albumine", valeur="28 g/L", valeur_num=28.0)], ) result = build_completude_checklist(dossier) assert len(result.checks) >= 1 check_e43 = next(c for c in result.checks if c.code == "E43") assert check_e43.verdict == "defendable" assert check_e43.score >= 70 # Vérifier les items alb = next(i for i in check_e43.items if i.element == "Albumine") assert alb.statut == "present_confirme" # 28 < 30 → confirmé assert alb.confirmation_detail is not None imc = next(i for i in check_e43.items if i.element == "IMC") assert imc.statut == "present_confirme" # 16.5 < 18.5 → confirmé def test_denutrition_albumine_haute(self): """E43 avec albumine 38 (> 30) → present_non_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition sévère", cim10_suggestion="E43"), sejour=Sejour(imc=16.5), bio=[BiologieCle(test="Albumine", valeur="38 g/L", valeur_num=38.0)], ) result = build_completude_checklist(dossier) check_e43 = next(c for c in result.checks if c.code == "E43") alb = next(i for i in check_e43.items if i.element == "Albumine") assert alb.statut == "present_non_confirme" assert alb.confirmation_detail is not None # Verdict doit refléter la non-confirmation assert check_e43.verdict == "fragile" def test_denutrition_sans_albumine(self): """E43 sans albumine → fragile.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition sévère", cim10_suggestion="E43"), sejour=Sejour(imc=16.5), ) result = build_completude_checklist(dossier) check_e43 = next(c for c in result.checks if c.code == "E43") assert check_e43.verdict == "fragile" alb = next(i for i in check_e43.items if i.element == "Albumine") assert alb.statut == "absent" def test_denutrition_sans_rien(self): """E43 sans albumine ni IMC → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition sévère", cim10_suggestion="E43"), ) result = build_completude_checklist(dossier) check_e43 = next(c for c in result.checks if c.code == "E43") assert check_e43.verdict == "indefendable" assert check_e43.score < 30 def test_e44_match_aussi(self): """E44.0 (dénutrition modérée) doit aussi matcher les règles de dénutrition.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition modérée", cim10_suggestion="E44.0"), sejour=Sejour(imc=19.5), bio=[BiologieCle(test="Albumine", valeur="32 g/L", valeur_num=32.0)], ) result = build_completude_checklist(dossier) assert any(c.code == "E44.0" for c in result.checks) check = next(c for c in result.checks if c.code == "E44.0") # 32 est dans [30-35] et 19.5 est dans [18.5-21] alb = next(i for i in check.items if i.element == "Albumine") assert alb.statut == "present_confirme" def test_e44_with_e43_seuils_not_applied(self): """E44 ne doit pas appliquer les seuils E43 (code_filter).""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition modérée", cim10_suggestion="E44.1"), sejour=Sejour(imc=19.0), bio=[BiologieCle(test="Albumine", valeur="32 g/L", valeur_num=32.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E44.1") # Les items avec code_filter=E43 ne doivent PAS apparaître pour E44 # Les items avec code_filter=E44 DOIVENT apparaître alb = next((i for i in check.items if i.element == "Albumine"), None) assert alb is not None # IMC doit utiliser les seuils E44 (range 18.5-21) imc = next((i for i in check.items if i.element == "IMC"), None) assert imc is not None assert imc.statut == "present_confirme" # 19.0 in [18.5, 21] # ── Tests anémie ───────────────────────────────────────────────────── class TestAnemie: def test_anemie_sans_hb(self): """D50 sans hémoglobine → indefendable (Hb est obligatoire).""" dossier = _make_dossier( das=[Diagnostic(texte="Anémie ferriprive", cim10_suggestion="D50.9")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "D50.9") assert check.verdict == "indefendable" def test_anemie_avec_hb_basse(self): """D64 avec Hb basse (homme) → present_confirme.""" dossier = _make_dossier( sejour=Sejour(sexe="M"), das=[Diagnostic(texte="Anémie", cim10_suggestion="D64.9")], bio=[BiologieCle(test="Hémoglobine", valeur="9.5 g/dL", valeur_num=9.5)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "D64.9") assert check.verdict == "defendable" hb = next(i for i in check.items if i.element == "Hémoglobine") assert hb.statut == "present_confirme" # 9.5 < 13 def test_anemie_hb_normale(self): """D64 avec Hb 14 (homme) → present_non_confirme.""" dossier = _make_dossier( sejour=Sejour(sexe="M"), das=[Diagnostic(texte="Anémie", cim10_suggestion="D64.9")], bio=[BiologieCle(test="Hémoglobine", valeur="14 g/dL", valeur_num=14.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "D64.9") hb = next(i for i in check.items if i.element == "Hémoglobine") assert hb.statut == "present_non_confirme" def test_anemie_seuil_femme(self): """D50 avec Hb 12.5 (femme) → non_confirme (seuil femme: < 12).""" dossier = _make_dossier( sejour=Sejour(sexe="F"), das=[Diagnostic(texte="Anémie", cim10_suggestion="D50.9")], bio=[BiologieCle(test="Hb", valeur="12.5 g/dL", valeur_num=12.5)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "D50.9") hb = next(i for i in check.items if i.element == "Hémoglobine") assert hb.statut == "present_non_confirme" # ── Tests insuffisance rénale ──────────────────────────────────────── class TestInsuffisanceRenale: def test_ir_avec_creatinine_haute(self): """N18 avec créatinine 180 (> 120) → present_confirme.""" dossier = _make_dossier( das=[Diagnostic(texte="IRC stade 3", cim10_suggestion="N18.3")], bio=[BiologieCle(test="Créatinine", valeur="180 µmol/L", valeur_num=180.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N18.3") assert check.verdict == "defendable" creat = next(i for i in check.items if i.element == "Créatinine") assert creat.statut == "present_confirme" def test_ir_creatinine_normale(self): """N18 avec créatinine 90 (≤ 120) → present_non_confirme.""" dossier = _make_dossier( das=[Diagnostic(texte="IRC", cim10_suggestion="N18.3")], bio=[BiologieCle(test="Créatinine", valeur="90 µmol/L", valeur_num=90.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N18.3") creat = next(i for i in check.items if i.element == "Créatinine") assert creat.statut == "present_non_confirme" # ── Tests sepsis ───────────────────────────────────────────────────── class TestSepsis: def test_sepsis_complet_confirme(self): """A41 avec CRP > 50 + leucocytes > 10 → confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Sepsis", cim10_suggestion="A41.9"), bio=[ BiologieCle(test="CRP", valeur="180 mg/L", valeur_num=180.0), BiologieCle(test="Leucocytes", valeur="15 G/L", valeur_num=15.0), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") assert check.verdict == "defendable" crp = next(i for i in check.items if i.element == "CRP") assert crp.statut == "present_confirme" # 180 > 50 leuco = next(i for i in check.items if i.element == "Leucocytes") assert leuco.statut == "present_confirme" # 15 hors [4-10] def test_sepsis_leucocytes_normaux(self): """A41 avec leucocytes 7 (dans norme) → present_non_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Sepsis", cim10_suggestion="A41.9"), bio=[ BiologieCle(test="CRP", valeur="180 mg/L", valeur_num=180.0), BiologieCle(test="Leucocytes", valeur="7 G/L", valeur_num=7.0), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") leuco = next(i for i in check.items if i.element == "Leucocytes") assert leuco.statut == "present_non_confirme" def test_sepsis_sans_bio(self): """A41 sans CRP ni leucocytes → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Sepsis", cim10_suggestion="A41.9"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") assert check.verdict == "indefendable" # ── Tests seuils spécifiques ───────────────────────────────────────── class TestSeuils: def test_pancreatite_lipase_haute(self): """K85 avec lipase 250 (> 180) → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Pancréatite aiguë", cim10_suggestion="K85"), bio=[BiologieCle(test="Lipase", valeur="250 UI/L", valeur_num=250.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "K85") lip = next(i for i in check.items if i.element == "Lipasémie") assert lip.statut == "present_confirme" def test_pancreatite_lipase_basse(self): """K85 avec lipase 120 (≤ 180) → present_non_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Pancréatite aiguë", cim10_suggestion="K85"), bio=[BiologieCle(test="Lipase", valeur="120 UI/L", valeur_num=120.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "K85") lip = next(i for i in check.items if i.element == "Lipasémie") assert lip.statut == "present_non_confirme" def test_obesite_imc_confirme(self): """E66 avec IMC 42 (> 30) → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Obésité morbide", cim10_suggestion="E66.0"), sejour=Sejour(imc=42.0, poids=130.0, taille=176.0), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E66.0") imc = next(i for i in check.items if i.element == "IMC") assert imc.statut == "present_confirme" def test_obesite_imc_non_confirme(self): """E66 avec IMC 25 (< 30) → present_non_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Obésité", cim10_suggestion="E66.9"), sejour=Sejour(imc=25.0, poids=75.0), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E66.9") imc = next(i for i in check.items if i.element == "IMC") assert imc.statut == "present_non_confirme" def test_hepatique_transaminases(self): """K72 avec ASAT 85 + ALAT 92 → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Insuffisance hépatique", cim10_suggestion="K72.0"), bio=[ BiologieCle(test="ASAT", valeur="85 UI/L", valeur_num=85.0), BiologieCle(test="ALAT", valeur="92 UI/L", valeur_num=92.0), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "K72.0") asat = next(i for i in check.items if i.element == "ASAT") alat = next(i for i in check.items if i.element == "ALAT") assert asat.statut == "present_confirme" assert alat.statut == "present_confirme" def test_ic_bnp_confirme(self): """I50 avec BNP 450 (> 100) → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Insuffisance cardiaque", cim10_suggestion="I50.0"), bio=[BiologieCle(test="BNP", valeur="450 pg/mL", valeur_num=450.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I50.0") bnp = next(i for i in check.items if "BNP" in i.element) assert bnp.statut == "present_confirme" def test_electrolytes_sodium_bas(self): """E87 avec Na 128 (< 135) → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="Hyponatrémie", cim10_suggestion="E87.1"), bio=[ BiologieCle(test="Sodium", valeur="128 mmol/L", valeur_num=128.0), BiologieCle(test="Potassium", valeur="4.2 mmol/L", valeur_num=4.2), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E87.1") na = next(i for i in check.items if i.element == "Sodium") assert na.statut == "present_confirme" # K normal (4.2 dans [3.5-5.0]) → non confirmé k = next(i for i in check.items if i.element == "Potassium") assert k.statut == "present_non_confirme" def test_bio_sans_valeur_num(self): """Bio présente mais sans valeur_num → statut 'present' (pas de confrontation seuil).""" dossier = _make_dossier( dp=Diagnostic(texte="IRC", cim10_suggestion="N18.3"), bio=[BiologieCle(test="Créatinine", valeur="élevée")], # pas de valeur_num ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N18.3") creat = next(i for i in check.items if i.element == "Créatinine") assert creat.statut == "present" # ── Tests preuves cliniques ────────────────────────────────────────── class TestPreuvesCliniques: def test_preuve_clinique_indirect(self): """Élément absent mais mentionné dans preuves_cliniques → present_indirect.""" dossier = _make_dossier( dp=Diagnostic( texte="Sepsis", cim10_suggestion="A41.9", preuves_cliniques=[ PreuveClinique( type="biologie", element="CRP 180 mg/L", interpretation="syndrome inflammatoire majeur", ), ], ), bio=[ # Pas de CRP dans biologie_cle, mais leucocytes oui BiologieCle(test="Leucocytes", valeur="15 G/L", valeur_num=15.0), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") crp = next(i for i in check.items if i.element == "CRP") assert crp.statut == "present_indirect" assert crp.valeur == "CRP 180 mg/L" assert "preuves cliniques" in crp.confirmation_detail.lower() def test_preuve_clinique_ne_remplace_pas_present(self): """Si l'élément est déjà présent, les preuves ne sont pas utilisées.""" dossier = _make_dossier( dp=Diagnostic( texte="Sepsis", cim10_suggestion="A41.9", preuves_cliniques=[ PreuveClinique( type="biologie", element="CRP 180 mg/L", interpretation="syndrome inflammatoire majeur", ), ], ), bio=[ BiologieCle(test="CRP", valeur="180 mg/L", valeur_num=180.0), BiologieCle(test="Leucocytes", valeur="15 G/L", valeur_num=15.0), ], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") crp = next(i for i in check.items if i.element == "CRP") # Doit être present_confirme, pas present_indirect assert crp.statut == "present_confirme" def test_preuve_imagerie_indirect(self): """Imagerie absente mais mentionnée dans preuves → present_indirect.""" dossier = _make_dossier( dp=Diagnostic( texte="AVC ischémique", cim10_suggestion="I63.9", preuves_cliniques=[ PreuveClinique( type="imagerie", element="IRM cérébral avec lésion ischémique", interpretation="AVC ischémique confirmé à l'IRM", ), ], ), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I63.9") img = next(i for i in check.items if "Scanner/IRM" in i.element) assert img.statut == "present_indirect" # ── Tests scoring pondéré ──────────────────────────────────────────── class TestScoringPondere: def test_present_confirme_full_weight(self): """present_confirme compte pour 1.0.""" dossier = _make_dossier( dp=Diagnostic(texte="IRC", cim10_suggestion="N18.3"), bio=[BiologieCle(test="Créatinine", valeur="200 µmol/L", valeur_num=200.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N18.3") assert check.score >= 70 def test_present_non_confirme_reduced_weight(self): """present_non_confirme compte pour 0.25 → score réduit.""" dossier = _make_dossier( dp=Diagnostic(texte="IRC", cim10_suggestion="N18.3"), bio=[BiologieCle(test="Créatinine", valeur="90 µmol/L", valeur_num=90.0)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N18.3") # present_non_confirme → 0.25 weight assert check.score < 70 def test_present_indirect_half_weight(self): """present_indirect compte pour 0.5.""" dossier = _make_dossier( dp=Diagnostic( texte="Sepsis", cim10_suggestion="A41.9", preuves_cliniques=[ PreuveClinique(type="biologie", element="CRP 200 mg/L", interpretation="CRP élevée"), PreuveClinique(type="biologie", element="Leucocytes 18 G/L", interpretation="hyperleucocytose"), ], ), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "A41.9") # Les deux obligatoires sont indirect (0.5 chacun) assert 20 <= check.score <= 60 # ── Tests tumeurs ──────────────────────────────────────────────────── class TestTumeurs: def test_tumeur_sans_anapath(self): """C34 sans ANAPATH → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Cancer bronchique", cim10_suggestion="C34.1"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "C34.1") assert check.verdict == "indefendable" assert "ANAPATH" in result.documents_manquants def test_tumeur_avec_anapath(self): """C34 avec ANAPATH dans les fichiers sources → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Cancer bronchique", cim10_suggestion="C34.1"), source_files=["CRH_patient.pdf", "ANAPATH_biopsie.pdf"], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "C34.1") assert check.verdict == "defendable" # ── Tests actes chirurgicaux ───────────────────────────────────────── class TestActesChirurgicaux: def test_chirurgie_sans_cro(self): """Acte CCAM chirurgical sans CRO → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Cholécystite", cim10_suggestion="K80.1"), actes=[ActeCCAM(texte="Cholécystectomie", code_ccam_suggestion="HMFC004")], ) result = build_completude_checklist(dossier) acte_check = next((c for c in result.checks if c.type_diag == "Acte"), None) assert acte_check is not None assert acte_check.verdict == "indefendable" assert "CRO" in result.documents_manquants def test_chirurgie_avec_cro(self): """Acte chirurgical avec CRO → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Cholécystite", cim10_suggestion="K80.1"), actes=[ActeCCAM(texte="Cholécystectomie", code_ccam_suggestion="HMFC004")], source_files=["CRH_patient.pdf", "CRO_cholecystectomie.pdf"], ) result = build_completude_checklist(dossier) acte_check = next((c for c in result.checks if c.type_diag == "Acte"), None) assert acte_check is not None assert acte_check.verdict == "defendable" # ── Tests embolie pulmonaire ───────────────────────────────────────── class TestEmboliePulmonaire: def test_ep_avec_scanner(self): """I26 avec angioscanner → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Embolie pulmonaire", cim10_suggestion="I26.0"), imagerie=[Imagerie(type="Angioscanner thoracique", conclusion="EP bilatérale")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I26.0") assert check.verdict == "defendable" def test_ep_sans_imagerie(self): """I26 sans imagerie → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Embolie pulmonaire", cim10_suggestion="I26.9"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I26.9") assert check.verdict == "indefendable" # ── Tests 8 nouvelles familles ─────────────────────────────────────── class TestNouvellesFamilles: def test_avc_avec_scanner(self): """I63 avec scanner cérébral → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="AVC ischémique", cim10_suggestion="I63.3"), imagerie=[Imagerie(type="Scanner cérébral", conclusion="Ischémie sylvienne gauche")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I63.3") assert check.verdict == "defendable" def test_avc_sans_imagerie(self): """I63 sans imagerie → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="AVC ischémique", cim10_suggestion="I63.3"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I63.3") assert check.verdict == "indefendable" def test_idm_avec_troponine_haute(self): """I21 avec troponine 0.5 (> 0.04) → present_confirme.""" dossier = _make_dossier( dp=Diagnostic(texte="IDM", cim10_suggestion="I21.0"), bio=[BiologieCle(test="Troponine I", valeur="0.5 ng/mL", valeur_num=0.5)], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I21.0") tropo = next(i for i in check.items if i.element == "Troponine") assert tropo.statut == "present_confirme" def test_idm_sans_troponine(self): """I21 sans troponine → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="IDM", cim10_suggestion="I21.0"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I21.0") assert check.verdict == "indefendable" def test_pneumopathie_avec_radio(self): """J18 avec radio thorax → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Pneumopathie", cim10_suggestion="J18.9"), imagerie=[Imagerie(type="Radio thorax", conclusion="Foyer alvéolaire droit")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "J18.9") assert check.verdict == "defendable" def test_tvp_avec_echodoppler(self): """I80 avec écho-doppler → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="TVP", cim10_suggestion="I80.2"), imagerie=[Imagerie(type="Écho-doppler veineux MI", conclusion="TVP fémorale")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I80.2") assert check.verdict == "defendable" def test_tvp_sans_imagerie(self): """I80 sans écho-doppler → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="TVP", cim10_suggestion="I80.2"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I80.2") assert check.verdict == "indefendable" def test_insuff_resp_avec_gds(self): """J96 avec gaz du sang → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Insuffisance respiratoire", cim10_suggestion="J96.0"), bio=[BiologieCle(test="Gaz du sang", valeur="PaO2 55 mmHg")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "J96.0") assert check.verdict == "defendable" def test_fracture_avec_radio(self): """S72 avec imagerie → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Fracture col fémoral", cim10_suggestion="S72.0"), imagerie=[Imagerie(type="Radiographie bassin", conclusion="Fracture cervicale vraie")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "S72.0") assert check.verdict == "defendable" def test_fracture_sans_imagerie(self): """S72 sans imagerie → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Fracture col fémoral", cim10_suggestion="S72.0"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "S72.0") assert check.verdict == "indefendable" def test_iu_avec_ecbu(self): """N39.0 avec ECBU → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Infection urinaire", cim10_suggestion="N39.0"), bio=[BiologieCle(test="ECBU", valeur="E.coli > 10^5")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N39.0") assert check.verdict == "defendable" def test_iu_sans_ecbu(self): """N39.0 sans ECBU → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Infection urinaire", cim10_suggestion="N39.0"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "N39.0") assert check.verdict == "indefendable" def test_fa_avec_ecg(self): """I48 avec ECG → defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="FA", cim10_suggestion="I48.0"), bio=[BiologieCle(test="ECG", valeur="FA rapide")], ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I48.0") assert check.verdict == "defendable" def test_fa_sans_ecg(self): """I48 sans ECG → indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="FA", cim10_suggestion="I48.0"), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "I48.0") assert check.verdict == "indefendable" def test_ait_g45(self): """G45 (AIT) doit aussi matcher la famille AVC/AIT.""" dossier = _make_dossier( dp=Diagnostic(texte="AIT", cim10_suggestion="G45.9"), imagerie=[Imagerie(type="IRM cérébral", conclusion="Pas de lésion récente")], ) result = build_completude_checklist(dossier) assert any(c.code == "G45.9" for c in result.checks) check = next(c for c in result.checks if c.code == "G45.9") assert check.verdict == "defendable" # ── Tests sans règle applicable ────────────────────────────────────── class TestSansRegle: def test_code_sans_regle(self): """Code sans règle applicable → pas de check.""" dossier = _make_dossier( dp=Diagnostic(texte="Grippe", cim10_suggestion="J11.1"), ) result = build_completude_checklist(dossier) assert not any(c.code == "J11.1" for c in result.checks) # Score global par défaut (pas de checks → pas de verdict) assert result.score_global == 100 def test_dossier_vide(self): """Dossier sans codes → pas de checks.""" dossier = _make_dossier() result = build_completude_checklist(dossier) assert result.checks == [] assert result.score_global == 100 # ── Tests verdict global ───────────────────────────────────────────── class TestVerdictGlobal: def test_mix_defendable_et_fragile(self): """Un code defendable + un fragile → verdict global fragile.""" dossier = _make_dossier( dp=Diagnostic(texte="IRC", cim10_suggestion="N18.3"), das=[Diagnostic(texte="Dénutrition", cim10_suggestion="E43")], bio=[BiologieCle(test="Créatinine", valeur="180", valeur_num=180.0)], # E43 n'a ni albumine ni IMC → indefendable ) result = build_completude_checklist(dossier) assert result.verdict_global in ("fragile", "indefendable") def test_tous_defendables(self): """Tous les codes defendables → verdict global defendable.""" dossier = _make_dossier( dp=Diagnostic(texte="IRC", cim10_suggestion="N18.3"), bio=[BiologieCle(test="Créatinine", valeur="180", valeur_num=180.0)], ) result = build_completude_checklist(dossier) assert result.verdict_global == "defendable" # ── Tests DAS ruled_out (ignorés) ──────────────────────────────────── class TestDasRuledOut: def test_das_ruled_out_ignore(self): """Un DAS ruled_out ne doit pas apparaître dans les checks.""" dossier = _make_dossier( das=[ Diagnostic(texte="Anémie", cim10_suggestion="D50.9", status="ruled_out"), ], ) result = build_completude_checklist(dossier) assert not any(c.code == "D50.9" for c in result.checks) # ── Tests documents présents ───────────────────────────────────────── class TestDocumentsPresents: def test_documents_listes(self): """Les types de documents sont listés.""" dossier = _make_dossier( dp=Diagnostic(texte="Test", cim10_suggestion="J18.9"), document_type="crh", source_files=["CRH_1.pdf", "CRO_op.pdf"], imagerie=[Imagerie(type="Radio thorax", conclusion="Normal")], ) result = build_completude_checklist(dossier) assert "crh" in result.documents_presents assert "cro" in result.documents_presents # ── Tests obésité ──────────────────────────────────────────────────── class TestObesite: def test_obesite_avec_imc_et_poids(self): """E66 avec IMC 42 + poids → defendable, score élevé.""" dossier = _make_dossier( dp=Diagnostic(texte="Obésité morbide", cim10_suggestion="E66.0"), sejour=Sejour(imc=42.0, poids=130.0, taille=176.0), ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E66.0") assert check.verdict == "defendable" assert check.score == 100 def test_obesite_sans_imc(self): """E66 sans IMC → fragile/indefendable.""" dossier = _make_dossier( dp=Diagnostic(texte="Obésité", cim10_suggestion="E66.9"), sejour=Sejour(poids=130.0), # Poids mais pas d'IMC ) result = build_completude_checklist(dossier) check = next(c for c in result.checks if c.code == "E66.9") assert check.verdict in ("fragile", "indefendable") # ── Test intégration pipeline ──────────────────────────────────────── class TestIntegration: def test_completude_dans_dossier_medical(self): """Le champ completude existe et accepte un CompletudeDossier.""" dossier = DossierMedical() assert dossier.completude is None dossier.completude = build_completude_checklist(dossier) assert isinstance(dossier.completude, CompletudeDossier) def test_serialization_json(self): """Le résultat se sérialise en JSON sans erreur.""" dossier = _make_dossier( dp=Diagnostic(texte="Dénutrition", cim10_suggestion="E43"), sejour=Sejour(imc=16.5), bio=[BiologieCle(test="Albumine", valeur="28 g/L", valeur_num=28.0)], ) result = build_completude_checklist(dossier) json_str = result.model_dump_json() assert "E43" in json_str assert "defendable" in json_str # Le nouveau champ doit apparaître assert "confirmation_detail" in json_str def test_confirmation_detail_in_model(self): """Le champ confirmation_detail est bien sérialisé.""" item = ItemCompletude( categorie="biologie", element="Albumine", statut="present_confirme", valeur="28 g/L", importance="obligatoire", confirmation_detail="Albumine 28 g/L < 30 → confirme E43", ) data = item.model_dump() assert data["confirmation_detail"] == "Albumine 28 g/L < 30 → confirme E43" assert data["statut"] == "present_confirme"