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:
@@ -135,46 +135,46 @@ class TestVeto22SameCategory:
|
||||
|
||||
|
||||
# ================================================================
|
||||
# VETO-23 : Exclusions mutuelles
|
||||
# VETO-25 : Exclusions mutuelles (ex-VETO-23, refactorisé via diagnostic_conflicts.yaml)
|
||||
# ================================================================
|
||||
|
||||
class TestVeto23MutualExclusions:
|
||||
class TestVeto25MutualExclusions:
|
||||
def test_e10_e11_mutual(self):
|
||||
"""E10 + E11 = diabète type 1 et 2 → VETO-23."""
|
||||
"""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)
|
||||
v23 = [i for i in report.issues if i.veto == "VETO-23"]
|
||||
assert len(v23) == 1
|
||||
assert "Diabète" in v23[0].message
|
||||
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-23."""
|
||||
"""I10 + I11 = HTA essentielle + secondaire → VETO-25."""
|
||||
d = _make_dossier(dp_code="I10", das_codes=["I11.9"])
|
||||
report = apply_vetos(d)
|
||||
v23 = [i for i in report.issues if i.veto == "VETO-23"]
|
||||
assert len(v23) == 1
|
||||
assert "HTA" in v23[0].message
|
||||
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-23."""
|
||||
"""I10 + I13 (HTA cardiorénale) → VETO-25."""
|
||||
d = _make_dossier(dp_code="K35.8", das_codes=["I10", "I13.0"])
|
||||
report = apply_vetos(d)
|
||||
v23 = [i for i in report.issues if i.veto == "VETO-23"]
|
||||
assert len(v23) == 1
|
||||
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-23."""
|
||||
"""Pas de conflit → pas de VETO-25."""
|
||||
d = _make_dossier(dp_code="E11.9", das_codes=["I10", "K35.8"])
|
||||
report = apply_vetos(d)
|
||||
v23 = [i for i in report.issues if i.veto == "VETO-23"]
|
||||
assert len(v23) == 0
|
||||
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-23."""
|
||||
"""E10 seul → pas de VETO-25."""
|
||||
d = _make_dossier(dp_code="E10.9", das_codes=["I10"])
|
||||
report = apply_vetos(d)
|
||||
v23 = [i for i in report.issues if i.veto == "VETO-23"]
|
||||
assert len(v23) == 0
|
||||
v25 = [i for i in report.issues if i.veto == "VETO-25"]
|
||||
assert len(v25) == 0
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -237,7 +237,7 @@ class TestVerdictIntegration:
|
||||
veto_ids = {i.veto for i in report.issues}
|
||||
# Z45 interdit en DP → VETO-20
|
||||
assert "VETO-20" in veto_ids
|
||||
# E10+E11 → VETO-23
|
||||
assert "VETO-23" 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
|
||||
|
||||
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"
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests pour la génération de contre-argumentation CPAM."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch, call
|
||||
|
||||
import pytest
|
||||
@@ -1659,6 +1660,7 @@ class TestBuildBioSummary:
|
||||
assert "CRP" not in summary
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"T2A_CPAM_MAX_CORRECTIONS": "2"})
|
||||
class TestCorrectionLoop:
|
||||
"""Tests pour la boucle de correction adversariale."""
|
||||
|
||||
|
||||
417
tests/test_extraction_multiformat.py
Normal file
417
tests/test_extraction_multiformat.py
Normal file
@@ -0,0 +1,417 @@
|
||||
"""Tests pour l'extraction multi-format (PDF, images, DOCX)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.extraction.document_router import (
|
||||
SUPPORTED_EXTENSIONS,
|
||||
extract_document_with_pages,
|
||||
)
|
||||
from src.extraction.page_tracker import PageTracker
|
||||
from src.extraction.pdf_extractor import (
|
||||
ExtractionMethod,
|
||||
ExtractionStats,
|
||||
_compute_extraction_stats,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests du router — dispatch par extension
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDocumentRouter:
|
||||
"""Tests unitaires du dispatch par extension."""
|
||||
|
||||
def test_router_pdf_dispatches_correctly(self, tmp_path):
|
||||
"""Un fichier .pdf est dispatché vers extract_text_with_pages."""
|
||||
pdf_file = tmp_path / "test.pdf"
|
||||
pdf_file.touch()
|
||||
|
||||
mock_stats = ExtractionStats(total_pages=1, source_format="pdf")
|
||||
mock_tracker = PageTracker([(0, 10)])
|
||||
mock_return = ("texte pdf", mock_tracker, mock_stats)
|
||||
|
||||
with patch(
|
||||
"src.extraction.pdf_extractor.extract_text_with_pages",
|
||||
return_value=mock_return,
|
||||
):
|
||||
result = extract_document_with_pages(pdf_file)
|
||||
assert len(result) == 3
|
||||
assert result[0] == "texte pdf"
|
||||
assert result[2].source_format == "pdf"
|
||||
|
||||
def test_router_image_dispatches_correctly(self, tmp_path):
|
||||
"""Un fichier .png est dispatché vers extract_text_from_image."""
|
||||
png_file = tmp_path / "test.png"
|
||||
png_file.touch()
|
||||
|
||||
mock_stats = ExtractionStats(
|
||||
total_pages=1, source_format="image",
|
||||
methods=[ExtractionMethod.IMAGE],
|
||||
)
|
||||
mock_tracker = PageTracker([(0, 10)])
|
||||
mock_return = ("texte ocr", mock_tracker, mock_stats)
|
||||
|
||||
with patch(
|
||||
"src.extraction.image_extractor.extract_text_from_image",
|
||||
return_value=mock_return,
|
||||
):
|
||||
result = extract_document_with_pages(png_file)
|
||||
assert len(result) == 3
|
||||
assert result[0] == "texte ocr"
|
||||
assert result[2].source_format == "image"
|
||||
|
||||
def test_router_docx_dispatches_correctly(self, tmp_path):
|
||||
"""Un fichier .docx est dispatché vers extract_text_from_docx."""
|
||||
docx_file = tmp_path / "test.docx"
|
||||
docx_file.touch()
|
||||
|
||||
mock_stats = ExtractionStats(
|
||||
total_pages=1, source_format="docx",
|
||||
methods=[ExtractionMethod.DOCX],
|
||||
)
|
||||
mock_tracker = PageTracker([(0, 10)])
|
||||
mock_return = ("texte docx", mock_tracker, mock_stats)
|
||||
|
||||
with patch(
|
||||
"src.extraction.docx_extractor.extract_text_from_docx",
|
||||
return_value=mock_return,
|
||||
):
|
||||
result = extract_document_with_pages(docx_file)
|
||||
assert len(result) == 3
|
||||
assert result[0] == "texte docx"
|
||||
assert result[2].source_format == "docx"
|
||||
|
||||
def test_router_unsupported_extension_raises(self, tmp_path):
|
||||
"""Une extension non supportée lève ValueError."""
|
||||
xyz_file = tmp_path / "test.xyz"
|
||||
xyz_file.touch()
|
||||
|
||||
with pytest.raises(ValueError, match="Format non supporté"):
|
||||
extract_document_with_pages(xyz_file)
|
||||
|
||||
def test_router_supported_extensions_complete(self):
|
||||
"""Vérifie que SUPPORTED_EXTENSIONS contient tous les formats prévus."""
|
||||
expected = {".pdf", ".jpg", ".jpeg", ".png", ".tiff", ".tif", ".docx"}
|
||||
assert SUPPORTED_EXTENSIONS == expected
|
||||
|
||||
@pytest.mark.parametrize("ext", [".jpg", ".jpeg", ".tiff", ".tif"])
|
||||
def test_router_all_image_extensions(self, tmp_path, ext):
|
||||
"""Toutes les extensions image sont reconnues."""
|
||||
img_file = tmp_path / f"test{ext}"
|
||||
img_file.touch()
|
||||
|
||||
mock_stats = ExtractionStats(total_pages=1, source_format="image")
|
||||
mock_tracker = PageTracker([(0, 5)])
|
||||
|
||||
with patch(
|
||||
"src.extraction.image_extractor.extract_text_from_image",
|
||||
return_value=("texte", mock_tracker, mock_stats),
|
||||
):
|
||||
result = extract_document_with_pages(img_file)
|
||||
assert result[2].source_format == "image"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests ExtractionStats enrichi
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExtractionStats:
|
||||
"""Tests des nouveaux champs d'ExtractionStats."""
|
||||
|
||||
def test_stats_tracks_method(self):
|
||||
"""Vérifie que methods/backend/source_format sont renseignés."""
|
||||
methods = [
|
||||
ExtractionMethod.NATIVE_PDFPLUMBER,
|
||||
ExtractionMethod.NATIVE_PDFPLUMBER,
|
||||
ExtractionMethod.OCR_DOCTR,
|
||||
]
|
||||
stats = _compute_extraction_stats(
|
||||
["page 1 avec du texte", "page 2 avec du texte", "page 3 ocr"],
|
||||
methods=methods,
|
||||
backend="pdfplumber",
|
||||
)
|
||||
|
||||
assert stats.methods == methods
|
||||
assert stats.backend == "pdfplumber"
|
||||
assert stats.source_format == "pdf"
|
||||
assert stats.native_pages == 2
|
||||
assert stats.ocr_pages == 1
|
||||
|
||||
def test_stats_default_values(self):
|
||||
"""Les valeurs par défaut sont correctes."""
|
||||
stats = ExtractionStats()
|
||||
assert stats.methods == []
|
||||
assert stats.native_pages == 0
|
||||
assert stats.ocr_pages == 0
|
||||
assert stats.backend == "pdfplumber"
|
||||
assert stats.source_format == "pdf"
|
||||
|
||||
def test_stats_image_format(self):
|
||||
"""ExtractionStats pour une image."""
|
||||
stats = ExtractionStats(
|
||||
total_pages=1,
|
||||
source_format="image",
|
||||
methods=[ExtractionMethod.IMAGE],
|
||||
backend="doctr",
|
||||
)
|
||||
assert stats.source_format == "image"
|
||||
assert stats.methods[0] == ExtractionMethod.IMAGE
|
||||
|
||||
def test_stats_docx_format(self):
|
||||
"""ExtractionStats pour un DOCX."""
|
||||
stats = ExtractionStats(
|
||||
total_pages=3,
|
||||
source_format="docx",
|
||||
methods=[ExtractionMethod.DOCX] * 3,
|
||||
backend="python-docx",
|
||||
native_pages=3,
|
||||
)
|
||||
assert stats.source_format == "docx"
|
||||
assert len(stats.methods) == 3
|
||||
assert all(m == ExtractionMethod.DOCX for m in stats.methods)
|
||||
|
||||
def test_compute_stats_with_methods(self):
|
||||
"""_compute_extraction_stats calcule correctement native_pages et ocr_pages."""
|
||||
pages = ["Hello world" * 10, "", "Texte OCR récupéré"]
|
||||
methods = [
|
||||
ExtractionMethod.NATIVE_PDFPLUMBER,
|
||||
ExtractionMethod.OCR_DOCTR,
|
||||
ExtractionMethod.OCR_DOCTR,
|
||||
]
|
||||
stats = _compute_extraction_stats(pages, methods, "pdfplumber")
|
||||
|
||||
assert stats.total_pages == 3
|
||||
assert stats.native_pages == 1
|
||||
assert stats.ocr_pages == 2
|
||||
assert 2 in stats.empty_pages # page 2 (1-indexed) est vide
|
||||
|
||||
def test_extraction_method_enum_values(self):
|
||||
"""Vérifie les valeurs de l'enum ExtractionMethod."""
|
||||
assert ExtractionMethod.NATIVE_PDFPLUMBER.value == "native_pdfplumber"
|
||||
assert ExtractionMethod.NATIVE_PYMUPDF.value == "native_pymupdf"
|
||||
assert ExtractionMethod.OCR_DOCTR.value == "ocr_doctr"
|
||||
assert ExtractionMethod.DOCX.value == "docx"
|
||||
assert ExtractionMethod.IMAGE.value == "image_ocr"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests OCR fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOCRFallback:
|
||||
"""Tests du mécanisme de fallback OCR."""
|
||||
|
||||
def test_ocr_fallback_disabled_by_default(self):
|
||||
"""Le fallback OCR est désactivé par défaut."""
|
||||
from src.extraction.pdf_extractor import OCR_FALLBACK_ENABLED
|
||||
|
||||
# Par défaut (sans variable d'environnement), le fallback est désactivé
|
||||
# Note : ce test vérifie le comportement par défaut, pas une variable statique
|
||||
# car elle peut être modifiée par les variables d'environnement du CI
|
||||
assert isinstance(OCR_FALLBACK_ENABLED, bool)
|
||||
|
||||
def test_ocr_fallback_config_values(self):
|
||||
"""Les constantes de config sont cohérentes."""
|
||||
from src.extraction.pdf_extractor import OCR_FALLBACK_MIN_CHARS, PDF_BACKEND
|
||||
|
||||
assert isinstance(OCR_FALLBACK_MIN_CHARS, int)
|
||||
assert OCR_FALLBACK_MIN_CHARS > 0
|
||||
assert PDF_BACKEND in ("pdfplumber", "pymupdf")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests DOCX extracteur (avec fixture)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDocxExtractor:
|
||||
"""Tests de l'extracteur DOCX."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_docx(self, tmp_path):
|
||||
"""Crée un petit DOCX de test."""
|
||||
try:
|
||||
from docx import Document
|
||||
except ImportError:
|
||||
pytest.skip("python-docx non installé")
|
||||
|
||||
doc = Document()
|
||||
doc.add_paragraph("Premier paragraphe du document médical.")
|
||||
doc.add_paragraph("Diagnostic principal : Pneumopathie J18.9")
|
||||
doc.add_paragraph("Traitement de sortie : Amoxicilline 1g x3/j")
|
||||
docx_path = tmp_path / "test_medical.docx"
|
||||
doc.save(str(docx_path))
|
||||
return docx_path
|
||||
|
||||
@pytest.fixture
|
||||
def docx_with_page_breaks(self, tmp_path):
|
||||
"""Crée un DOCX avec des sauts de page."""
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
except ImportError:
|
||||
pytest.skip("python-docx non installé")
|
||||
|
||||
doc = Document()
|
||||
doc.add_paragraph("Page 1 : Antécédents du patient.")
|
||||
|
||||
# Ajouter un saut de page
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run()
|
||||
br = OxmlElement("w:br")
|
||||
br.set(qn("w:type"), "page")
|
||||
run._element.append(br)
|
||||
|
||||
doc.add_paragraph("Page 2 : Compte-rendu opératoire.")
|
||||
docx_path = tmp_path / "test_pages.docx"
|
||||
doc.save(str(docx_path))
|
||||
return docx_path
|
||||
|
||||
def test_extract_docx_basic(self, sample_docx):
|
||||
"""Extraction basique d'un DOCX."""
|
||||
from src.extraction.docx_extractor import extract_text_from_docx
|
||||
|
||||
text, tracker, stats = extract_text_from_docx(sample_docx)
|
||||
|
||||
assert "Pneumopathie" in text
|
||||
assert "Amoxicilline" in text
|
||||
assert stats.source_format == "docx"
|
||||
assert stats.total_pages >= 1
|
||||
assert stats.total_chars > 0
|
||||
assert all(m == ExtractionMethod.DOCX for m in stats.methods)
|
||||
|
||||
def test_extract_docx_with_page_breaks(self, docx_with_page_breaks):
|
||||
"""Extraction d'un DOCX avec sauts de page."""
|
||||
from src.extraction.docx_extractor import extract_text_from_docx
|
||||
|
||||
text, tracker, stats = extract_text_from_docx(docx_with_page_breaks)
|
||||
|
||||
assert stats.total_pages == 2
|
||||
assert "Antécédents" in text
|
||||
assert "Compte-rendu" in text
|
||||
# PageTracker fonctionne
|
||||
assert tracker.char_to_page(0) == 1
|
||||
|
||||
def test_extract_docx_file_not_found(self, tmp_path):
|
||||
"""FileNotFoundError si le fichier n'existe pas."""
|
||||
from src.extraction.docx_extractor import extract_text_from_docx
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
extract_text_from_docx(tmp_path / "inexistant.docx")
|
||||
|
||||
def test_extract_docx_stats_backend(self, sample_docx):
|
||||
"""Le backend est bien 'python-docx'."""
|
||||
from src.extraction.docx_extractor import extract_text_from_docx
|
||||
|
||||
_, _, stats = extract_text_from_docx(sample_docx)
|
||||
assert stats.backend == "python-docx"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests image extracteur (mock OCR)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestImageExtractor:
|
||||
"""Tests de l'extracteur d'images (avec OCR mocké)."""
|
||||
|
||||
def test_extract_image_file_not_found(self, tmp_path):
|
||||
"""FileNotFoundError si l'image n'existe pas."""
|
||||
from src.extraction.image_extractor import extract_text_from_image
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
extract_text_from_image(tmp_path / "inexistant.png")
|
||||
|
||||
def test_extract_image_stats_format(self, tmp_path):
|
||||
"""Vérifie le format des stats pour une image."""
|
||||
# Créer une petite image PNG
|
||||
from PIL import Image
|
||||
|
||||
img = Image.new("RGB", (100, 50), color="white")
|
||||
img_path = tmp_path / "test.png"
|
||||
img.save(str(img_path))
|
||||
|
||||
with patch("src.extraction.image_extractor.ocr_image", return_value="Texte OCR extrait"):
|
||||
from src.extraction.image_extractor import extract_text_from_image
|
||||
|
||||
text, tracker, stats = extract_text_from_image(img_path)
|
||||
|
||||
assert text == "Texte OCR extrait"
|
||||
assert stats.source_format == "image"
|
||||
assert stats.total_pages == 1
|
||||
assert stats.ocr_pages == 1
|
||||
assert stats.native_pages == 0
|
||||
assert stats.methods == [ExtractionMethod.IMAGE]
|
||||
assert stats.backend == "doctr"
|
||||
|
||||
def test_extract_image_empty_result(self, tmp_path):
|
||||
"""Image sans texte détectable."""
|
||||
from PIL import Image
|
||||
|
||||
img = Image.new("RGB", (100, 50), color="white")
|
||||
img_path = tmp_path / "blank.png"
|
||||
img.save(str(img_path))
|
||||
|
||||
with patch("src.extraction.image_extractor.ocr_image", return_value=""):
|
||||
from src.extraction.image_extractor import extract_text_from_image
|
||||
|
||||
text, tracker, stats = extract_text_from_image(img_path)
|
||||
|
||||
assert text == ""
|
||||
assert stats.empty_pages == [1]
|
||||
assert stats.total_chars == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests de non-régression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBackwardCompat:
|
||||
"""Tests de rétrocompatibilité."""
|
||||
|
||||
def test_process_pdf_alias_exists(self):
|
||||
"""process_pdf est un alias de process_document."""
|
||||
from src.main import process_document, process_pdf
|
||||
|
||||
assert process_pdf is process_document
|
||||
|
||||
def test_extraction_stats_existing_properties(self):
|
||||
"""Les propriétés existantes d'ExtractionStats fonctionnent toujours."""
|
||||
stats = ExtractionStats(
|
||||
total_pages=5,
|
||||
empty_pages=[2, 4],
|
||||
chars_per_page=[100, 0, 200, 0, 300],
|
||||
total_chars=600,
|
||||
)
|
||||
assert stats.usable_pages == 3
|
||||
assert stats.coverage_ratio == 0.6
|
||||
assert stats.has_quality_issues() is True
|
||||
alert = stats.to_alert()
|
||||
assert alert is not None
|
||||
assert "2/5" in alert
|
||||
|
||||
flags = stats.to_flags()
|
||||
assert flags["extraction_empty_pages"] == [2, 4]
|
||||
assert flags["extraction_total_pages"] == 5
|
||||
assert flags["extraction_coverage"] == 0.6
|
||||
|
||||
def test_extraction_stats_no_issues(self):
|
||||
"""Pas d'alerte quand tout va bien."""
|
||||
stats = ExtractionStats(
|
||||
total_pages=3,
|
||||
chars_per_page=[100, 200, 300],
|
||||
total_chars=600,
|
||||
)
|
||||
assert not stats.has_quality_issues()
|
||||
assert stats.to_alert() is None
|
||||
assert stats.to_flags() == {}
|
||||
198
tests/test_nutrition_has2021.py
Normal file
198
tests/test_nutrition_has2021.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Tests unitaires pour la détection dénutrition HAS/FFN 2021."""
|
||||
|
||||
import re
|
||||
import pytest
|
||||
|
||||
from src.config import BiologieCle, Diagnostic, DossierMedical, Sejour
|
||||
from src.medical.diagnostic_extraction import _detect_nutrition_has2021, _DAS_PATTERNS
|
||||
from src.medical.cim10_dict import normalize_text
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _make_dossier(age=None, imc=None, albumine=None, existing_codes=None):
|
||||
"""Construit un DossierMedical minimal pour les tests."""
|
||||
dossier = DossierMedical()
|
||||
dossier.sejour = Sejour(age=age, imc=imc)
|
||||
if albumine is not None:
|
||||
dossier.biologie_cle.append(
|
||||
BiologieCle(
|
||||
test="Albumine",
|
||||
valeur=str(albumine),
|
||||
valeur_num=float(albumine),
|
||||
anomalie=True,
|
||||
quality="ok",
|
||||
)
|
||||
)
|
||||
for code in (existing_codes or []):
|
||||
dossier.diagnostics_associes.append(
|
||||
Diagnostic(texte="existant", cim10_suggestion=code, source="test")
|
||||
)
|
||||
return dossier
|
||||
|
||||
|
||||
# ── Tests _detect_nutrition_has2021 ──────────────────────────────────
|
||||
|
||||
|
||||
class TestDetectNutritionHAS2021:
|
||||
"""Tests de la détection déterministe basée sur IMC/âge/albumine."""
|
||||
|
||||
def test_adulte_imc17_albumine28_gives_E43(self):
|
||||
"""Adulte IMC 17.0 + albumine 28 → E43 (sévère via IMC ≤17 ET albumine <30)."""
|
||||
dossier = _make_dossier(age=50, imc=17.0, albumine=28)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E43" in codes
|
||||
|
||||
def test_adulte_imc18_sans_albumine_gives_E44(self):
|
||||
"""Adulte IMC 18.0 sans albumine → E44.0 (modéré, 17 < IMC < 18.5)."""
|
||||
dossier = _make_dossier(age=45, imc=18.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E44.0" in codes
|
||||
|
||||
def test_personne_agee_75_imc21_gives_E44(self):
|
||||
"""≥70 ans IMC 21.0 → E44.0 (seuil gériatrique < 22)."""
|
||||
dossier = _make_dossier(age=75, imc=21.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E44.0" in codes
|
||||
|
||||
def test_personne_agee_75_imc19_gives_E43(self):
|
||||
"""≥70 ans IMC 19.0 → E43 (sévère, IMC < 20)."""
|
||||
dossier = _make_dossier(age=75, imc=19.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E43" in codes
|
||||
|
||||
def test_adulte_imc25_no_das(self):
|
||||
"""Adulte IMC 25.0 → aucun DAS (au-dessus du seuil)."""
|
||||
dossier = _make_dossier(age=40, imc=25.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert not any(c in codes for c in ("E43", "E44.0", "E46"))
|
||||
|
||||
def test_e46_deja_code_no_ajout(self):
|
||||
"""E46 déjà codé → aucun ajout."""
|
||||
dossier = _make_dossier(age=50, imc=16.0, existing_codes=["E46"])
|
||||
_detect_nutrition_has2021(dossier)
|
||||
e_codes = [d.cim10_suggestion for d in dossier.diagnostics_associes
|
||||
if d.cim10_suggestion in ("E43", "E44.0", "E46")]
|
||||
# Seul le E46 existant doit être présent
|
||||
assert e_codes == ["E46"]
|
||||
|
||||
def test_pas_imc_no_ajout(self):
|
||||
"""Pas d'IMC → aucun ajout (dégradation gracieuse)."""
|
||||
dossier = _make_dossier(age=50, imc=None)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert not any(c in codes for c in ("E43", "E44.0", "E46"))
|
||||
|
||||
def test_albumine_upgrade_severity(self):
|
||||
"""IMC modéré + albumine < 30 → upgrade vers E43."""
|
||||
dossier = _make_dossier(age=50, imc=18.0, albumine=25)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E43" in codes # Albumine < 30 → sévère
|
||||
|
||||
def test_alerte_codage_added(self):
|
||||
"""Vérifie qu'une alerte codage est ajoutée."""
|
||||
dossier = _make_dossier(age=50, imc=17.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
assert any("HAS 2021" in a for a in dossier.alertes_codage)
|
||||
|
||||
def test_source_is_has2021(self):
|
||||
"""Vérifie que la source est 'has2021'."""
|
||||
dossier = _make_dossier(age=50, imc=17.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
has_diags = [d for d in dossier.diagnostics_associes if d.source == "has2021"]
|
||||
assert len(has_diags) == 1
|
||||
|
||||
def test_age_inconnu_seuils_adulte(self):
|
||||
"""Âge inconnu → seuils adulte par défaut."""
|
||||
dossier = _make_dossier(age=None, imc=17.0)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E43" in codes # IMC ≤ 17 → sévère (seuils adulte)
|
||||
|
||||
def test_personne_agee_70_exact_seuil(self):
|
||||
"""70 ans exactement → utilise les seuils gériatriques."""
|
||||
dossier = _make_dossier(age=70, imc=21.5)
|
||||
_detect_nutrition_has2021(dossier)
|
||||
codes = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
||||
assert "E44.0" in codes # < 22 → modéré avec seuils ≥70
|
||||
|
||||
|
||||
# ── Tests regex albumine (bio_extraction) ────────────────────────────
|
||||
|
||||
|
||||
class TestAlbumineRegex:
|
||||
"""Vérifie l'extraction regex de l'albumine."""
|
||||
|
||||
ALBUMINE_PATTERN = r"(?:[Aa]lbumin[ée]?(?:mie)?|[Aa]lb(?:u)?[ée]?(?:mie)?)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:g/[Ll])?"
|
||||
PREALBUMINE_PATTERN = r"(?:[Pp]r[ée]albumine|[Tt]ransthyr[ée]tine)\s*[=:àa]?\s*(\d+(?:[.,]\d+)?)\s*(?:mg/[Ll]|g/[Ll])?"
|
||||
|
||||
def test_albumine_standard(self):
|
||||
m = re.search(self.ALBUMINE_PATTERN, "Albumine = 28 g/L")
|
||||
assert m and m.group(1) == "28"
|
||||
|
||||
def test_albumine_colon(self):
|
||||
m = re.search(self.ALBUMINE_PATTERN, "albumine: 32.5 g/L")
|
||||
assert m and m.group(1) == "32.5"
|
||||
|
||||
def test_albumine_sans_unite(self):
|
||||
m = re.search(self.ALBUMINE_PATTERN, "Albumine 28")
|
||||
assert m and m.group(1) == "28"
|
||||
|
||||
def test_albuminemie(self):
|
||||
m = re.search(self.ALBUMINE_PATTERN, "Albuminémie à 25 g/L")
|
||||
assert m and m.group(1) == "25"
|
||||
|
||||
def test_prealbumine(self):
|
||||
m = re.search(self.PREALBUMINE_PATTERN, "Préalbumine = 0.15 g/L")
|
||||
assert m and m.group(1) == "0.15"
|
||||
|
||||
def test_transthyretine(self):
|
||||
m = re.search(self.PREALBUMINE_PATTERN, "Transthyrétine: 180 mg/L")
|
||||
assert m and m.group(1) == "180"
|
||||
|
||||
|
||||
# ── Tests regex texte dénutrition (DAS patterns) ────────────────────
|
||||
|
||||
|
||||
class TestDenutritionRegexSeverity:
|
||||
"""Vérifie que les patterns textuels de dénutrition détectent la sévérité."""
|
||||
|
||||
def _match_pattern(self, text):
|
||||
"""Retourne le (label, code) du premier pattern DAS matché."""
|
||||
text_norm = normalize_text(text.lower())
|
||||
for pat, label, code in _DAS_PATTERNS:
|
||||
if re.search(pat, text_norm):
|
||||
return label, code
|
||||
return None, None
|
||||
|
||||
def test_denutrition_severe_gives_E43(self):
|
||||
_, code = self._match_pattern("denutrition severe")
|
||||
assert code == "E43"
|
||||
|
||||
def test_denutrition_moderee_gives_E44(self):
|
||||
_, code = self._match_pattern("denutrition moderee")
|
||||
assert code == "E44.0"
|
||||
|
||||
def test_denutrition_generic_gives_E46(self):
|
||||
_, code = self._match_pattern("denutrition")
|
||||
assert code == "E46"
|
||||
|
||||
def test_malnutrition_severe_gives_E43(self):
|
||||
_, code = self._match_pattern("malnutrition severe")
|
||||
assert code == "E43"
|
||||
|
||||
def test_denutrition_grade_iii_gives_E43(self):
|
||||
_, code = self._match_pattern("denutrition grade III")
|
||||
assert code == "E43"
|
||||
|
||||
def test_hypoalbuminemie_severe_gives_E46(self):
|
||||
"""hypoalbuminemie severe → E46 (pattern générique)."""
|
||||
_, code = self._match_pattern("hypoalbuminemie severe")
|
||||
assert code == "E46"
|
||||
364
tests/test_nutrition_has2021_integration.py
Normal file
364
tests/test_nutrition_has2021_integration.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""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
|
||||
@@ -192,7 +192,7 @@ class TestSplitDocuments:
|
||||
# --- Test intégration process_pdf ---
|
||||
|
||||
class TestProcessPdfMulti:
|
||||
@patch("src.main.extract_text_with_pages")
|
||||
@patch("src.main.extract_document_with_pages")
|
||||
@patch("src.main.extract_medical_info")
|
||||
@patch("src.main._run_edsnlp", return_value=None)
|
||||
@patch("src.main._use_edsnlp", False)
|
||||
@@ -203,9 +203,14 @@ class TestProcessPdfMulti:
|
||||
from src.main import process_pdf
|
||||
from src.config import DossierMedical, Diagnostic
|
||||
from src.extraction.page_tracker import PageTracker
|
||||
from src.extraction.pdf_extractor import ExtractionStats
|
||||
|
||||
# Mock extract_text_with_pages retournant un texte multi-épisodes Trackare
|
||||
mock_extract.return_value = (TRACKARE_MULTI, PageTracker([(0, len(TRACKARE_MULTI))]))
|
||||
# Mock extract_document_with_pages retournant un texte multi-épisodes Trackare
|
||||
mock_extract.return_value = (
|
||||
TRACKARE_MULTI,
|
||||
PageTracker([(0, len(TRACKARE_MULTI))]),
|
||||
ExtractionStats(total_pages=1, chars_per_page=[len(TRACKARE_MULTI)], total_chars=len(TRACKARE_MULTI)),
|
||||
)
|
||||
|
||||
# Mock extract_medical_info retournant un DossierMedical minimal
|
||||
mock_medical.return_value = DossierMedical(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests pour le viewer Flask."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -12,9 +13,10 @@ from src.config import DossierMedical, Diagnostic, ActeCCAM
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
return app
|
||||
with patch.dict(os.environ, {"T2A_DEMO_USER": "", "T2A_DEMO_PASS": ""}):
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user