Files
t2a_v2/tests/test_completude.py
dom 4e2b4bd946 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>
2026-03-07 16:48:10 +01:00

886 lines
39 KiB
Python

"""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"