- Réorganisation data/referentiels/ : pdfs/, dicts/, user/ (structure unifiée) - Fix badges "Source absente" sur page admin référentiels - Ré-indexation COCOA 2025 (555 → 1451 chunks, couverture 94%) - Fix VRAM OOM : embeddings forcés CPU via T2A_EMBED_CPU - Nouveaux modules : document_router, docx_extractor, image_extractor, ocr_engine - Module complétude (quality/completude.py + config YAML) - Template DIM (synthèse dimensionnelle) - Gunicorn config + systemd service t2a-viewer - Suppression t2a_install_rag_cleanup/ (copie obsolète) - Suppression scripts/ et scripts_t2a_v2/ (anciens benchmarks) - Suppression 81 fichiers _doc.txt de test - Cache Ollama : TTL configurable, corrections loader YAML - Dashboard : améliorations templates (base, index, detail, cpam, validation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
9.6 KiB
Python
244 lines
9.6 KiB
Python
"""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
|