feat: qualité DP Phase 2 — filtre OCR étendu, abréviations médicales, promotion DAS→DP
- 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>
This commit is contained in:
150
tests/test_decision_engine.py
Normal file
150
tests/test_decision_engine.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user