"""Tests unitaires pour les règles ATIH (veto engine). Couvre : VETO-20 (Z en DP), VETO-21 (R en DP), VETO-22 (même catégorie DP+DAS), VETO-23 (exclusions mutuelles), VETO-24 (traumatisme sans cause externe). """ import pytest from src.config import Diagnostic, DossierMedical, Sejour from src.quality.veto_engine import apply_vetos def _make_dossier(dp_code=None, das_codes=None, dp_texte="test", das_textes=None): """Helper : crée un DossierMedical minimal avec DP + DAS.""" dossier = DossierMedical(sejour=Sejour()) if dp_code: dossier.diagnostic_principal = Diagnostic( texte=dp_texte, cim10_suggestion=dp_code, source_excerpt="preuve test", cim10_confidence="medium", ) for i, code in enumerate(das_codes or []): texte = (das_textes[i] if das_textes and i < len(das_textes) else f"das test {code}") dossier.diagnostics_associes.append(Diagnostic( texte=texte, cim10_suggestion=code, source_excerpt="preuve test", cim10_confidence="medium", )) return dossier # ================================================================ # VETO-20 : Z code interdit en DP # ================================================================ class TestVeto20ZCodeDP: def test_z_code_forbidden_dp(self): """Z00 en DP doit déclencher VETO-20.""" d = _make_dossier(dp_code="Z00.8", das_codes=["I10"]) report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 1 assert "Z00" in v20[0].message def test_z51_whitelist_dp(self): """Z51 (chimiothérapie) est autorisé en DP — pas de VETO-20.""" d = _make_dossier(dp_code="Z51.1", das_codes=["C50.9"]) report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 0 def test_z09_whitelist_dp(self): """Z09 (suivi post-traitement) est autorisé en DP.""" d = _make_dossier(dp_code="Z09.0", das_codes=["C50.9"]) report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 0 def test_z54_whitelist_dp(self): """Z54 (convalescence) est autorisé en DP.""" d = _make_dossier(dp_code="Z54.0") report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 0 def test_z_in_das_no_veto(self): """Z code en DAS ne déclenche PAS VETO-20.""" d = _make_dossier(dp_code="I10", das_codes=["Z87.1"]) report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 0 def test_z45_forbidden(self): """Z45 (ajustement de dispositif) n'est pas dans la whitelist.""" d = _make_dossier(dp_code="Z45.0") report = apply_vetos(d) v20 = [i for i in report.issues if i.veto == "VETO-20"] assert len(v20) == 1 # ================================================================ # VETO-21 : Code R (symptôme) en DP # ================================================================ class TestVeto21RCodeDP: def test_r_code_dp_no_precise_das(self): """R code en DP sans diagnostic précis en DAS → MEDIUM.""" d = _make_dossier(dp_code="R10.9", das_codes=["R50.9", "Z87.1"]) report = apply_vetos(d) v21 = [i for i in report.issues if i.veto == "VETO-21"] assert len(v21) == 1 assert v21[0].severity == "MEDIUM" def test_r_code_dp_with_precise_das(self): """R code en DP avec diagnostic précis en DAS → LOW (informatif).""" d = _make_dossier(dp_code="R10.9", das_codes=["K35.8"]) report = apply_vetos(d) v21 = [i for i in report.issues if i.veto == "VETO-21"] assert len(v21) == 1 assert v21[0].severity == "LOW" def test_non_r_dp_no_veto(self): """DP hors chapitre R → pas de VETO-21.""" d = _make_dossier(dp_code="K35.8", das_codes=["R10.9"]) report = apply_vetos(d) v21 = [i for i in report.issues if i.veto == "VETO-21"] assert len(v21) == 0 # ================================================================ # VETO-22 : Même catégorie 3 chars DP + DAS # ================================================================ class TestVeto22SameCategory: def test_same_3char_dp_das(self): """J45.0 en DP + J45.9 en DAS → VETO-22.""" d = _make_dossier(dp_code="J45.0", das_codes=["J45.9", "I10"]) report = apply_vetos(d) v22 = [i for i in report.issues if i.veto == "VETO-22"] assert len(v22) == 1 assert "J45" in v22[0].message def test_exact_duplicate_no_veto22(self): """Code identique DP=DAS est VETO-06, pas VETO-22.""" d = _make_dossier(dp_code="J45.0", das_codes=["J45.0"]) report = apply_vetos(d) v22 = [i for i in report.issues if i.veto == "VETO-22"] assert len(v22) == 0 # traité par VETO-06 def test_different_categories(self): """Catégories différentes → pas de VETO-22.""" d = _make_dossier(dp_code="K35.8", das_codes=["I10", "E11.9"]) report = apply_vetos(d) v22 = [i for i in report.issues if i.veto == "VETO-22"] assert len(v22) == 0 # ================================================================ # VETO-25 : Exclusions mutuelles (ex-VETO-23, refactorisé via diagnostic_conflicts.yaml) # ================================================================ class TestVeto25MutualExclusions: def test_e10_e11_mutual(self): """E10 + E11 = diabète type 1 et 2 → VETO-25.""" d = _make_dossier(dp_code="E10.9", das_codes=["E11.9", "I10"]) report = apply_vetos(d) v25 = [i for i in report.issues if i.veto == "VETO-25"] assert len(v25) == 1 assert "Diabète" in v25[0].message def test_i10_i11_mutual(self): """I10 + I11 = HTA essentielle + secondaire → VETO-25.""" d = _make_dossier(dp_code="I10", das_codes=["I11.9"]) report = apply_vetos(d) v25 = [i for i in report.issues if i.veto == "VETO-25"] assert len(v25) == 1 assert "HTA" in v25[0].message def test_i10_i13_mutual(self): """I10 + I13 (HTA cardiorénale) → VETO-25.""" d = _make_dossier(dp_code="K35.8", das_codes=["I10", "I13.0"]) report = apply_vetos(d) v25 = [i for i in report.issues if i.veto == "VETO-25"] assert len(v25) == 1 def test_no_mutual_exclusion(self): """Pas de conflit → pas de VETO-25.""" d = _make_dossier(dp_code="E11.9", das_codes=["I10", "K35.8"]) report = apply_vetos(d) v25 = [i for i in report.issues if i.veto == "VETO-25"] assert len(v25) == 0 def test_e10_alone_no_veto(self): """E10 seul → pas de VETO-25.""" d = _make_dossier(dp_code="E10.9", das_codes=["I10"]) report = apply_vetos(d) v25 = [i for i in report.issues if i.veto == "VETO-25"] assert len(v25) == 0 # ================================================================ # VETO-24 : Traumatisme sans cause externe # ================================================================ class TestVeto24InjuryExternalCause: def test_injury_without_external(self): """S72.0 sans V/W/X/Y → VETO-24.""" d = _make_dossier(dp_code="S72.0", das_codes=["I10"]) report = apply_vetos(d) v24 = [i for i in report.issues if i.veto == "VETO-24"] assert len(v24) == 1 def test_injury_with_external(self): """S72.0 + W19 → pas de VETO-24.""" d = _make_dossier(dp_code="S72.0", das_codes=["W19.0"]) report = apply_vetos(d) v24 = [i for i in report.issues if i.veto == "VETO-24"] assert len(v24) == 0 def test_t_complication_no_veto(self): """T82 (complication de dispositif) n'est PAS un traumatisme → pas de VETO-24.""" d = _make_dossier(dp_code="T82.1", das_codes=["I10"]) report = apply_vetos(d) v24 = [i for i in report.issues if i.veto == "VETO-24"] assert len(v24) == 0 def test_no_injury_no_veto(self): """Pas de code S/T → pas de VETO-24.""" d = _make_dossier(dp_code="K35.8", das_codes=["I10"]) report = apply_vetos(d) v24 = [i for i in report.issues if i.veto == "VETO-24"] assert len(v24) == 0 def test_injury_in_das_without_external(self): """Code S en DAS sans externe → VETO-24.""" d = _make_dossier(dp_code="K35.8", das_codes=["S06.0"]) report = apply_vetos(d) v24 = [i for i in report.issues if i.veto == "VETO-24"] assert len(v24) == 1 # ================================================================ # Tests de non-régression : verdicts globaux # ================================================================ class TestVerdictIntegration: def test_clean_dossier_pass(self): """Dossier propre → PASS (ou NEED_INFO si VETO-02 capte).""" d = _make_dossier(dp_code="K35.8", das_codes=["I10", "E11.9"]) report = apply_vetos(d) # Pas de HARD → pas de FAIL assert report.verdict != "FAIL" def test_multiple_atih_vetos(self): """Plusieurs vetos ATIH combinés.""" d = _make_dossier(dp_code="Z45.0", das_codes=["E10.9", "E11.9", "S72.0"]) report = apply_vetos(d) veto_ids = {i.veto for i in report.issues} # Z45 interdit en DP → VETO-20 assert "VETO-20" in veto_ids # E10+E11 → VETO-25 (ex-VETO-23, via diagnostic_conflicts.yaml) assert "VETO-25" in veto_ids # S72 sans externe → VETO-24 assert "VETO-24" in veto_ids