Files
t2a_v2/tests/test_nutrition_has2021_integration.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

365 lines
12 KiB
Python

"""Tests d'intégration : détection dénutrition HAS/FFN 2021 sur dossiers réalistes.
4 cas cliniques réalistes passés dans le pipeline complet extract_medical_info.
Vérifie l'interaction entre regex bio, détection HAS 2021, conflits, et sévérité CMA.
"""
import pytest
from src.config import DossierMedical
from src.medical.cim10_extractor import extract_medical_info
# ── Cas 1 : Personne âgée dénutrie (≥70 ans, seuils gériatriques) ───
class TestCas1PersonneAgeeDenutrie:
"""Mme D., 81 ans, hospitalisée pour pneumopathie.
IMC 20.5 → sous le seuil gériatrique < 22 → dénutrition modérée.
Albumine 27 g/L → < 30 → critère de sévérité → upgrade vers E43.
Attendu : E43 (dénutrition sévère) détecté par HAS 2021.
"""
@pytest.fixture
def dossier(self) -> DossierMedical:
parsed = {
"type": "crh",
"patient": {
"sexe": "F",
"date_naissance": "15/03/1943",
},
"sejour": {
"date_entree": "10/01/2025",
"date_sortie": "20/01/2025",
},
"diagnostics": [],
"signes_vitaux": {"imc": 20.5, "poids_kg": 48, "taille_cm": 153},
}
text = """\
Votre patiente née le 15/03/1943 a été hospitalisée du 10/01/2025 au 20/01/2025.
Antécédents :
- Hypertension artérielle
- Fibrillation auriculaire paroxystique
- Arthrose invalidante
Au total : Pneumopathie basale droite d'évolution favorable sous antibiothérapie.
Biologie d'entrée :
CRP = 145 mg/L
Albumine = 27 g/L
Créatinine = 95 µmol/L
Sodium = 138 mmol/L
Potassium = 4.1 mmol/L
Hémoglobine = 10.2 g/dL
IMC: 20.5
TTT de sortie :
Amoxicilline 1g matin et soir pendant 5 jours
Paracétamol si besoin
Devenir : retour à domicile."""
return extract_medical_info(parsed, text)
def test_age_81_ans(self, dossier):
assert dossier.sejour.age == 81
def test_imc_extrait(self, dossier):
assert dossier.sejour.imc == 20.5
def test_albumine_extraite(self, dossier):
"""L'albumine doit être extraite par le nouveau regex."""
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
assert len(alb) >= 1
assert alb[0].valeur_num == 27.0
def test_denutrition_severe_E43(self, dossier):
"""IMC 20.5 (modéré ≥70 ans) + albumine 27 (<30) → E43 sévère."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E43" in codes, f"E43 attendu, trouvé : {codes}"
def test_source_has2021(self, dossier):
has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"]
assert len(has_diags) == 1
assert has_diags[0].cim10_suggestion == "E43"
def test_alerte_has2021(self, dossier):
assert any("HAS 2021" in a for a in dossier.alertes_codage)
def test_pneumopathie_detectee(self, dossier):
"""Le DP/DAS pneumopathie ne doit pas être impacté."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
# Pneumopathie J18.9 ou DP
all_codes = codes.copy()
if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion:
all_codes.add(dossier.diagnostic_principal.cim10_suggestion)
assert "J18.9" in all_codes
def test_hta_detectee(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "I10" in codes
# ── Cas 2 : Patient obèse ET dénutri (conflit E66 + E43 = MEDIUM) ───
class TestCas2ObeseDenutri:
"""M. B., 58 ans, hospitalisé pour pancréatite aiguë sur obésité.
IMC 35.2 → obèse (E66.0). Dénutrition sévère mentionnée dans le texte (E43).
Le conflit E66/dénutrition doit être MEDIUM (pas HARD) selon HAS 2021.
La coexistence est cliniquement possible (sarcopénie de l'obèse).
DP fourni via Trackare (K85.9) pour éviter que NUKE-3 ne réorganise les codes.
"""
@pytest.fixture
def dossier(self) -> DossierMedical:
parsed = {
"type": "trackare",
"patient": {
"sexe": "M",
"date_naissance": "22/07/1967",
},
"sejour": {
"date_entree": "05/02/2025",
"date_sortie": "12/02/2025",
},
"diagnostics": [
{
"type": "Principal",
"statut": "actif",
"code_cim10": "K85.9",
"libelle": "Pancréatite aiguë",
}
],
"signes_vitaux": {"imc": 35.2, "poids_kg": 110, "taille_cm": 177},
}
text = """\
Pancréatite aiguë sur terrain d'obésité morbide.
Dénutrition sévère protéique avec sarcopénie documentée,
perte de poids de 15 kg en 3 mois.
Antécédents :
- Diabète type 2
- Tabagisme actif
Biologie :
CRP = 145 mg/L
Albumine = 25 g/L
Lipasémie = 1200 UI/L
HbA1c = 7.8 %
Hémoglobine = 13.5 g/dL
IMC: 35.2
TTT de sortie :
Metformine 1000mg matin et soir
Paracétamol si besoin
Devenir : retour à domicile."""
return extract_medical_info(parsed, text)
def test_dp_pancreatite(self, dossier):
"""DP = K85.9 (fourni par Trackare)."""
assert dossier.diagnostic_principal is not None
assert dossier.diagnostic_principal.cim10_suggestion == "K85.9"
def test_obesite_E66_detectee(self, dossier):
"""L'obésité doit être détectée via IMC ≥ 30."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E66.0" in codes
def test_denutrition_severe_regex_E43(self, dossier):
"""'Dénutrition sévère' dans le texte → E43 via regex."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E43" in codes, f"E43 attendu via regex 'denutrition severe', trouvé : {codes}"
def test_coexistence_E66_E43(self, dossier):
"""E66.0 et E43 doivent coexister (pas de blocage HARD)."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E66.0" in codes and "E43" in codes
def test_albumine_extraite(self, dossier):
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
assert len(alb) >= 1
assert alb[0].valeur_num == 25.0
def test_diabete_detecte(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E11.9" in codes
def test_tabagisme_detecte(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "F17.2" in codes
# ── Cas 3 : Adulte dénutri modéré sans albumine (IMC seul) ──────────
class TestCas3AdulteDenutriModere:
"""Mme L., 45 ans, hospitalisée pour angiocholite.
IMC 17.8 → entre 17 et 18.5 → dénutrition modérée (E44.0) par HAS 2021.
Pas d'albumine → pas d'upgrade de sévérité.
"""
@pytest.fixture
def dossier(self) -> DossierMedical:
parsed = {
"type": "crh",
"patient": {
"sexe": "F",
"date_naissance": "12/09/1979",
},
"sejour": {
"date_entree": "15/03/2025",
"date_sortie": "21/03/2025",
},
"diagnostics": [],
"signes_vitaux": {"imc": 17.8, "poids_kg": 48, "taille_cm": 164},
}
text = """\
Votre patiente née le 12/09/1979 a été hospitalisée du 15/03/2025 au 21/03/2025.
Antécédents :
- Lithiases vésiculaires
- Anorexie restrictive ancienne (adolescence)
Au total : Angiocholite sur lithiase du cholédoque traitée par CPRE
puis cholécystectomie par cœlioscopie.
Biologie d'entrée :
CRP = 92 mg/L
ASAT = 180 UI/L
ALAT = 210 UI/L
Bilirubine totale = 45 µmol/L
Lipasémie = 890 UI/L
Hémoglobine = 11.8 g/dL
Leucocytes = 12.5 G/L
IMC: 17.8
TTT de sortie :
Paracétamol 1g x3/jour
Spasfon si besoin
Devenir : retour à domicile."""
return extract_medical_info(parsed, text)
def test_imc_178(self, dossier):
assert dossier.sejour.imc == 17.8
def test_denutrition_moderee_E44(self, dossier):
"""IMC 17.8 adulte → E44.0 (modéré, 17 < IMC < 18.5)."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E44.0" in codes, f"E44.0 attendu, trouvé : {codes}"
def test_pas_E43(self, dossier):
"""Pas de sévère sans albumine basse ni IMC ≤ 17."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E43" not in codes
def test_pas_albumine_extraite(self, dossier):
"""Pas d'albumine dans le texte → pas d'extraction."""
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
assert len(alb) == 0
def test_angiocholite_detectee(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
all_codes = codes.copy()
if dossier.diagnostic_principal and dossier.diagnostic_principal.cim10_suggestion:
all_codes.add(dossier.diagnostic_principal.cim10_suggestion)
assert "K83.0" in all_codes
def test_cholecystectomie_detectee(self, dossier):
acte_codes = {a.code_ccam_suggestion for a in dossier.actes_ccam}
assert "HMFC004" in acte_codes
def test_alerte_has2021(self, dossier):
assert any("HAS 2021" in a for a in dossier.alertes_codage)
# ── Cas 4 : IMC normal, pas de dénutrition (contrôle négatif) ───────
class TestCas4ControleNegatif:
"""M. R., 55 ans, hospitalisé pour colique hépatique.
IMC 26.3 → au-dessus de tous les seuils → aucune dénutrition.
Albumine 38 g/L → normale.
Vérifie que la détection HAS 2021 ne produit pas de faux positif.
"""
@pytest.fixture
def dossier(self) -> DossierMedical:
parsed = {
"type": "crh",
"patient": {
"sexe": "M",
"date_naissance": "30/11/1969",
},
"sejour": {
"date_entree": "01/04/2025",
"date_sortie": "03/04/2025",
},
"diagnostics": [],
"signes_vitaux": {"imc": 26.3, "poids_kg": 82, "taille_cm": 176},
}
text = """\
Votre patient né le 30/11/1969 a été hospitalisé du 01/04/2025 au 03/04/2025.
Antécédents :
- Hypertension artérielle
- Dyslipidémie
Au total : Colique hépatique sur lithiase vésiculaire.
Bonne évolution. Cholécystectomie programmée à distance.
Biologie :
CRP = 12 mg/L
Albumine = 38 g/L
ASAT = 35 UI/L
ALAT = 42 UI/L
Hémoglobine = 14.5 g/dL
Créatinine = 78 µmol/L
IMC: 26.3
TTT de sortie :
Paracétamol si besoin
Spasfon si besoin
Devenir : retour à domicile."""
return extract_medical_info(parsed, text)
def test_imc_normal(self, dossier):
assert dossier.sejour.imc == 26.3
def test_albumine_normale_extraite(self, dossier):
"""Albumine 38 g/L → extraite mais normale."""
alb = [b for b in dossier.biologie_cle if b.test == "Albumine"]
assert len(alb) >= 1
assert alb[0].valeur_num == 38.0
def test_pas_de_denutrition(self, dossier):
"""IMC 26.3 + albumine 38 → aucun code E40-E46."""
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
nutrition_codes = {c for c in codes if c and c.startswith("E4") and c[:3] in
("E40", "E41", "E42", "E43", "E44", "E45", "E46")}
assert not nutrition_codes, f"Faux positif dénutrition : {nutrition_codes}"
def test_pas_alerte_has2021(self, dossier):
"""Aucune alerte HAS 2021 ne doit apparaître."""
assert not any("HAS 2021" in a for a in dossier.alertes_codage)
def test_hta_detectee(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "I10" in codes
def test_dyslipidemie_detectee(self, dossier):
codes = {d.cim10_suggestion for d in dossier.diagnostics_associes}
assert "E78.5" in codes