refactor: réorganisation référentiels, nouveaux modules extraction, nettoyage code obsolète
- 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>
This commit is contained in:
885
tests/test_completude.py
Normal file
885
tests/test_completude.py
Normal file
@@ -0,0 +1,885 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user