- Filtre OCR : regex étendu (opérateurs +-*/), artefacts temporels (années), seuil digits abaissé 0.50→0.48 - Dictionnaire 41 abréviations médicales françaises (BMR, BPCO, SDRA, OAP, IDM, SCA, AVC, ACFA, SIDA, TDAH, etc.) avec expand_medical_abbreviations() appelé sur diagnostics Trackare et DAS LLM - Promotion DAS→DP : si aucun DP extrait, le meilleur DAS (scoring pertinence/confiance/spécificité) est promu avec traçabilité RULE-DAS-TO-DP - 95 nouveaux tests (OCR, abréviations, promotion, scoring, non-régression) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
"""Tests unitaires pour le moteur de décisions (promotion DAS→DP)."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from src.config import CodeDecision, Diagnostic, DossierMedical
|
|
from src.quality.decision_engine import (
|
|
_das_promotion_score,
|
|
apply_decisions,
|
|
decision_summaries,
|
|
)
|
|
|
|
|
|
def _make_dossier(dp=None, das_list=None):
|
|
"""Helper : crée un DossierMedical minimal."""
|
|
d = DossierMedical()
|
|
d.diagnostic_principal = dp
|
|
d.diagnostics_associes = das_list or []
|
|
return d
|
|
|
|
|
|
def _make_diag(texte, code, confidence="high", source="trackare", status=None, cim10_final=None):
|
|
"""Helper : crée un Diagnostic avec suggestion et optionnellement un final pré-rempli."""
|
|
return Diagnostic(
|
|
texte=texte,
|
|
cim10_suggestion=code,
|
|
cim10_confidence=confidence,
|
|
source=source,
|
|
status=status,
|
|
cim10_final=cim10_final,
|
|
)
|
|
|
|
|
|
# --- Scoring ---
|
|
|
|
class TestDasPromotionScore:
|
|
def test_pathology_beats_symptom(self):
|
|
patho = _make_diag("Pancréatite", "K85.9", cim10_final="K85.9")
|
|
symptom = _make_diag("Douleur abdominale", "R10.4", cim10_final="R10.4")
|
|
assert _das_promotion_score(patho) > _das_promotion_score(symptom)
|
|
|
|
def test_symptom_beats_zcode(self):
|
|
symptom = _make_diag("Douleur abdominale", "R10.4", cim10_final="R10.4")
|
|
zcode = _make_diag("Antécédent", "Z87.1", cim10_final="Z87.1")
|
|
assert _das_promotion_score(symptom) > _das_promotion_score(zcode)
|
|
|
|
def test_high_confidence_beats_medium(self):
|
|
high = _make_diag("Pancréatite", "K85.9", confidence="high", cim10_final="K85.9")
|
|
med = _make_diag("Pancréatite", "K85.9", confidence="medium", cim10_final="K85.9")
|
|
assert _das_promotion_score(high) > _das_promotion_score(med)
|
|
|
|
def test_longer_code_more_specific(self):
|
|
short = _make_diag("Pancréatite", "K85", cim10_final="K85")
|
|
long = _make_diag("Pancréatite biliaire", "K85.1", cim10_final="K85.1")
|
|
assert _das_promotion_score(long) > _das_promotion_score(short)
|
|
|
|
|
|
# --- Promotion DAS→DP ---
|
|
|
|
@patch("src.quality.decision_engine.load_reference_ranges", return_value={})
|
|
@patch("src.quality.decision_engine.load_bio_rules", return_value={})
|
|
class TestPromotionDasToDP:
|
|
|
|
@patch("src.quality.decision_engine.rule_enabled", return_value=True)
|
|
@patch("src.quality.decision_engine.cim10_validate", return_value=(True, "label"))
|
|
def test_promote_best_das_when_no_dp(self, mock_validate, mock_rule, mock_bio, mock_ref):
|
|
"""DP absent + DAS valides → meilleur DAS promu (pathologie > symptôme > Z)."""
|
|
das1 = _make_diag("Douleur abdominale", "R10.4", confidence="high")
|
|
das2 = _make_diag("Pancréatite aiguë", "K85.9", confidence="high")
|
|
das3 = _make_diag("Antécédent chirurgical", "Z87.1", confidence="medium")
|
|
dossier = _make_dossier(dp=None, das_list=[das1, das2, das3])
|
|
|
|
apply_decisions(dossier)
|
|
|
|
assert dossier.diagnostic_principal is not None
|
|
assert dossier.diagnostic_principal.cim10_final == "K85.9"
|
|
assert dossier.diagnostic_principal.cim10_decision.action == "PROMOTE_DP"
|
|
assert "RULE-DAS-TO-DP" in dossier.diagnostic_principal.cim10_decision.applied_rules
|
|
# Le DAS promu est retiré de la liste
|
|
codes_das = [d.cim10_suggestion for d in dossier.diagnostics_associes]
|
|
assert "K85.9" not in codes_das
|
|
assert len(dossier.diagnostics_associes) == 2
|
|
|
|
@patch("src.quality.decision_engine.rule_enabled", return_value=True)
|
|
@patch("src.quality.decision_engine.cim10_validate", return_value=(True, "label"))
|
|
def test_no_promotion_when_dp_present(self, mock_validate, mock_rule, mock_bio, mock_ref):
|
|
"""DP déjà présent → pas de promotion."""
|
|
dp = _make_diag("Cholécystite aiguë", "K81.0", confidence="high")
|
|
das1 = _make_diag("HTA", "I10", confidence="high")
|
|
dossier = _make_dossier(dp=dp, das_list=[das1])
|
|
|
|
apply_decisions(dossier)
|
|
|
|
assert dossier.diagnostic_principal.cim10_suggestion == "K81.0"
|
|
assert len(dossier.diagnostics_associes) == 1
|
|
|
|
@patch("src.quality.decision_engine.rule_enabled", return_value=True)
|
|
@patch("src.quality.decision_engine.cim10_validate", return_value=(True, "label"))
|
|
def test_no_promotion_for_ruled_out(self, mock_validate, mock_rule, mock_bio, mock_ref):
|
|
"""DAS ruled_out ne doit pas être promu."""
|
|
das1 = _make_diag("Thrombopénie", "D69.6", status="ruled_out")
|
|
dossier = _make_dossier(dp=None, das_list=[das1])
|
|
|
|
apply_decisions(dossier)
|
|
|
|
# D69.6 est ruled_out donc cim10_final est None → pas candidat
|
|
assert dossier.diagnostic_principal is None
|
|
|
|
@patch("src.quality.decision_engine.rule_enabled", return_value=True)
|
|
@patch("src.quality.decision_engine.cim10_validate", return_value=(False, None))
|
|
def test_no_promotion_without_cim10_final(self, mock_validate, mock_rule, mock_bio, mock_ref):
|
|
"""DAS sans cim10_final (code invalide) ne doit pas être promu."""
|
|
das1 = _make_diag("Diagnostic inconnu", "XXX.X")
|
|
dossier = _make_dossier(dp=None, das_list=[das1])
|
|
|
|
apply_decisions(dossier)
|
|
|
|
# Code invalide → cim10_final non rempli → pas candidat
|
|
assert dossier.diagnostic_principal is None
|
|
|
|
def test_no_promotion_when_rule_disabled(self, mock_bio, mock_ref):
|
|
"""RULE-DAS-TO-DP désactivée → pas de promotion."""
|
|
def _rule_enabled_selective(rule_id):
|
|
if rule_id == "RULE-DAS-TO-DP":
|
|
return False
|
|
return True
|
|
|
|
with patch("src.quality.decision_engine.rule_enabled", side_effect=_rule_enabled_selective):
|
|
with patch("src.quality.decision_engine.cim10_validate", return_value=(True, "label")):
|
|
das1 = _make_diag("Pancréatite aiguë", "K85.9", confidence="high")
|
|
dossier = _make_dossier(dp=None, das_list=[das1])
|
|
apply_decisions(dossier)
|
|
assert dossier.diagnostic_principal is None
|
|
|
|
|
|
# --- Summary handler ---
|
|
|
|
class TestDecisionSummaryPromoteDP:
|
|
def test_promote_dp_summary(self):
|
|
dp = _make_diag("Pancréatite aiguë", "K85.9", cim10_final="K85.9")
|
|
dp.cim10_decision = CodeDecision(
|
|
action="PROMOTE_DP",
|
|
final_code="K85.9",
|
|
applied_rules=["RULE-DAS-TO-DP"],
|
|
)
|
|
dossier = _make_dossier(dp=dp)
|
|
lines = decision_summaries(dossier)
|
|
assert any("PROMOTE_DP" in line or "promu en DP" in line for line in lines)
|
|
assert any("K85.9" in line for line in lines)
|