feat: tests étendus pour src/medical/dp_finalizer.py (34 tests)
Couvre les cas non couverts dans test_dp_finalizer.py : - R6 Z-code non whitelisté remplacé par DAS - Fonctions internes (_family3, _code_in_candidates, _has_strong_evidence) - _apply_r5 en appel direct (Z whitelisté, R-code avec candidat non-R) - Détection source Trackare par document_type/reason/evidence - Cas limites (code None, candidats vides, pas de dp_selection) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
340
tests/test_dp_finalizer_ext.py
Normal file
340
tests/test_dp_finalizer_ext.py
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
"""Tests étendus pour src/medical/dp_finalizer.py.
|
||||||
|
|
||||||
|
Couvre les cas non couverts dans test_dp_finalizer.py :
|
||||||
|
- R6 — Z-code non whitelisté remplacé par DAS
|
||||||
|
- Fonctions internes (_family3, _code_in_candidates, _has_strong_evidence)
|
||||||
|
- finalize_dp avec source Trackare détectée par evidence/reason
|
||||||
|
- Cas limites : codes None, candidats vides, Z whitelistés en R6
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.config import DossierMedical, DPSelection, DPCandidate, Diagnostic, Sejour
|
||||||
|
from src.medical.dp_finalizer import (
|
||||||
|
finalize_dp,
|
||||||
|
decide_dp_final,
|
||||||
|
_family3,
|
||||||
|
_code_in_candidates,
|
||||||
|
_has_strong_evidence,
|
||||||
|
_apply_r5,
|
||||||
|
_make_selection,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _sel(
|
||||||
|
code: str | None = None,
|
||||||
|
term: str = "",
|
||||||
|
verdict: str = "REVIEW",
|
||||||
|
confidence: str = "medium",
|
||||||
|
evidence: list[str] | None = None,
|
||||||
|
reason: str = "",
|
||||||
|
candidates: list[DPCandidate] | None = None,
|
||||||
|
) -> DPSelection:
|
||||||
|
return DPSelection(
|
||||||
|
chosen_code=code,
|
||||||
|
chosen_term=term or (code or ""),
|
||||||
|
verdict=verdict,
|
||||||
|
confidence=confidence,
|
||||||
|
evidence=evidence or [],
|
||||||
|
reason=reason,
|
||||||
|
candidates=candidates or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cand(code: str, term: str = "", score: float = 0.0) -> DPCandidate:
|
||||||
|
return DPCandidate(index=0, term=term or code, code=code, score=score)
|
||||||
|
|
||||||
|
|
||||||
|
def _dossier(
|
||||||
|
doc_type: str = "crh",
|
||||||
|
dp_selection: DPSelection | None = None,
|
||||||
|
das: list[Diagnostic] | None = None,
|
||||||
|
) -> DossierMedical:
|
||||||
|
return DossierMedical(
|
||||||
|
document_type=doc_type,
|
||||||
|
sejour=Sejour(),
|
||||||
|
dp_selection=dp_selection,
|
||||||
|
diagnostics_associes=das or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Fonctions internes
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFamily3:
|
||||||
|
@pytest.mark.parametrize("code,expected", [
|
||||||
|
("K81.0", "K81"),
|
||||||
|
("K81", "K81"),
|
||||||
|
("I26.9", "I26"),
|
||||||
|
("Z51.1", "Z51"),
|
||||||
|
(None, ""),
|
||||||
|
("", ""),
|
||||||
|
])
|
||||||
|
def test_extraction(self, code, expected):
|
||||||
|
assert _family3(code) == expected
|
||||||
|
|
||||||
|
|
||||||
|
class TestCodeInCandidates:
|
||||||
|
def test_exact_match(self):
|
||||||
|
sel = _sel(candidates=[_cand("I26.9"), _cand("K85.1")])
|
||||||
|
assert _code_in_candidates("I26.9", sel) is True
|
||||||
|
|
||||||
|
def test_family3_match(self):
|
||||||
|
sel = _sel(candidates=[_cand("I26.9")])
|
||||||
|
assert _code_in_candidates("I26.0", sel) is True # same family
|
||||||
|
|
||||||
|
def test_no_match(self):
|
||||||
|
sel = _sel(candidates=[_cand("I26.9")])
|
||||||
|
assert _code_in_candidates("K85.1", sel) is False
|
||||||
|
|
||||||
|
def test_none_code(self):
|
||||||
|
sel = _sel(candidates=[_cand("I26.9")])
|
||||||
|
assert _code_in_candidates(None, sel) is False
|
||||||
|
|
||||||
|
def test_empty_candidates(self):
|
||||||
|
sel = _sel(candidates=[])
|
||||||
|
assert _code_in_candidates("I26.9", sel) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestHasStrongEvidence:
|
||||||
|
def test_no_evidence(self):
|
||||||
|
sel = _sel(evidence=[])
|
||||||
|
assert _has_strong_evidence(sel) is False
|
||||||
|
|
||||||
|
def test_only_trackare_source(self):
|
||||||
|
"""'Source: Trackare' seul n'est pas une preuve forte."""
|
||||||
|
sel = _sel(evidence=["Source: Trackare (codage établissement)"])
|
||||||
|
assert _has_strong_evidence(sel) is False
|
||||||
|
|
||||||
|
def test_strong_evidence(self):
|
||||||
|
sel = _sel(evidence=["Diagnostic de sortie: «Embolie pulmonaire»"])
|
||||||
|
assert _has_strong_evidence(sel) is True
|
||||||
|
|
||||||
|
def test_mixed_evidence(self):
|
||||||
|
sel = _sel(evidence=[
|
||||||
|
"Source: Trackare (codage établissement)",
|
||||||
|
"Conclusion CRH: «Pancréatite biliaire»",
|
||||||
|
])
|
||||||
|
assert _has_strong_evidence(sel) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakeSelection:
|
||||||
|
def test_preserves_source_candidates(self):
|
||||||
|
source = _sel(candidates=[_cand("I26.9", "EP", 5.0)])
|
||||||
|
result = _make_selection(
|
||||||
|
code="I26.9", term="EP", verdict="CONFIRMED",
|
||||||
|
confidence="high", evidence=[], reason="test",
|
||||||
|
source_sel=source,
|
||||||
|
)
|
||||||
|
assert len(result.candidates) == 1
|
||||||
|
assert result.candidates[0].code == "I26.9"
|
||||||
|
|
||||||
|
def test_no_source(self):
|
||||||
|
result = _make_selection(
|
||||||
|
code="I26.9", term="EP", verdict="CONFIRMED",
|
||||||
|
confidence="high", evidence=[], reason="test",
|
||||||
|
)
|
||||||
|
assert result.candidates == []
|
||||||
|
assert result.debug_scores is None
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# R5 — _apply_r5 (direct call)
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplyR5Direct:
|
||||||
|
def test_z_code_non_whitelisted_forced_review(self):
|
||||||
|
dp = _sel(code="Z95.5", verdict="CONFIRMED", confidence="high", evidence=[])
|
||||||
|
dp, flags, alertes = _apply_r5(dp, None, allow_symptom_dp=False)
|
||||||
|
assert dp.verdict == "REVIEW"
|
||||||
|
assert flags.get("z_code_dp_review") is True
|
||||||
|
|
||||||
|
def test_z_code_whitelisted_untouched(self):
|
||||||
|
dp = _sel(code="Z51.1", verdict="CONFIRMED", confidence="high", evidence=[])
|
||||||
|
dp, flags, alertes = _apply_r5(dp, None, allow_symptom_dp=False)
|
||||||
|
assert dp.verdict == "CONFIRMED"
|
||||||
|
assert "z_code_dp_review" not in flags
|
||||||
|
|
||||||
|
def test_r_code_with_non_r_candidate(self):
|
||||||
|
crh = _sel(candidates=[_cand("R06.0"), _cand("J18.9")])
|
||||||
|
dp = _sel(code="R06.0", verdict="CONFIRMED", confidence="high", evidence=[])
|
||||||
|
dp, flags, alertes = _apply_r5(dp, crh, allow_symptom_dp=False)
|
||||||
|
assert dp.verdict == "REVIEW"
|
||||||
|
assert flags.get("r_code_dp_with_non_r_candidate") is True
|
||||||
|
|
||||||
|
def test_r_code_allowed_by_flag(self):
|
||||||
|
crh = _sel(candidates=[_cand("R06.0"), _cand("J18.9")])
|
||||||
|
dp = _sel(code="R06.0", verdict="CONFIRMED", confidence="high", evidence=[])
|
||||||
|
dp, flags, alertes = _apply_r5(dp, crh, allow_symptom_dp=True)
|
||||||
|
assert dp.verdict == "CONFIRMED"
|
||||||
|
|
||||||
|
def test_review_dp_not_changed(self):
|
||||||
|
"""R5 ne touche que les CONFIRMED."""
|
||||||
|
dp = _sel(code="Z95.5", verdict="REVIEW", confidence="medium", evidence=[])
|
||||||
|
dp, flags, alertes = _apply_r5(dp, None, allow_symptom_dp=False)
|
||||||
|
assert dp.verdict == "REVIEW"
|
||||||
|
assert "z_code_dp_review" not in flags # pas de flag car déjà REVIEW
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# R6 — Z-code remplacé par DAS (via finalize_dp)
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestR6ZcodeReplacedByDas:
|
||||||
|
def test_z_code_replaced_by_das(self):
|
||||||
|
"""Z-code non whitelisté en DP → DAS high/medium non-R non-Z promu."""
|
||||||
|
sel = _sel(
|
||||||
|
code="Z96.6", # non whitelisté
|
||||||
|
verdict="CONFIRMED",
|
||||||
|
evidence=["Source: Trackare (codage établissement)"],
|
||||||
|
reason="DP Trackare",
|
||||||
|
)
|
||||||
|
das = [
|
||||||
|
Diagnostic(texte="Pancréatite", cim10_suggestion="K85.9", cim10_confidence="high"),
|
||||||
|
]
|
||||||
|
d = _dossier(doc_type="trackare", dp_selection=sel, das=das)
|
||||||
|
finalize_dp(d)
|
||||||
|
|
||||||
|
assert d.dp_final is not None
|
||||||
|
assert d.dp_final.chosen_code == "K85.9"
|
||||||
|
assert d.quality_flags.get("z_code_replaced_by_das") is True
|
||||||
|
assert any("K85.9" in a for a in d.alertes_codage)
|
||||||
|
|
||||||
|
def test_z_code_whitelisted_not_replaced(self):
|
||||||
|
"""Z-code whitelisté en DP → pas de remplacement."""
|
||||||
|
sel = _sel(
|
||||||
|
code="Z51.1", # whitelisté
|
||||||
|
verdict="CONFIRMED",
|
||||||
|
evidence=["Source: Trackare (codage établissement)"],
|
||||||
|
reason="DP Trackare",
|
||||||
|
)
|
||||||
|
das = [
|
||||||
|
Diagnostic(texte="Pancréatite", cim10_suggestion="K85.9", cim10_confidence="high"),
|
||||||
|
]
|
||||||
|
d = _dossier(doc_type="trackare", dp_selection=sel, das=das)
|
||||||
|
finalize_dp(d)
|
||||||
|
|
||||||
|
assert d.dp_final.chosen_code == "Z51.1"
|
||||||
|
assert "z_code_replaced_by_das" not in d.quality_flags
|
||||||
|
|
||||||
|
def test_z_code_no_eligible_das(self):
|
||||||
|
"""Z-code non whitelisté mais aucun DAS éligible → pas de remplacement."""
|
||||||
|
sel = _sel(
|
||||||
|
code="Z96.6",
|
||||||
|
verdict="CONFIRMED",
|
||||||
|
evidence=["Source: Trackare (codage établissement)"],
|
||||||
|
reason="DP Trackare",
|
||||||
|
)
|
||||||
|
das = [
|
||||||
|
Diagnostic(texte="Symptôme R", cim10_suggestion="R10.4", cim10_confidence="high"),
|
||||||
|
Diagnostic(texte="Z-code", cim10_suggestion="Z87.1", cim10_confidence="low"),
|
||||||
|
]
|
||||||
|
d = _dossier(doc_type="trackare", dp_selection=sel, das=das)
|
||||||
|
finalize_dp(d)
|
||||||
|
|
||||||
|
# R10.4 est R-code, Z87.1 est Z-code → aucun éligible
|
||||||
|
assert "z_code_replaced_by_das" not in d.quality_flags
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# finalize_dp — détection source Trackare
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFinalizeDpSourceDetection:
|
||||||
|
def test_trackare_detected_by_document_type(self):
|
||||||
|
sel = _sel(code="K81.0", verdict="CONFIRMED", evidence=[], reason="CRH DP")
|
||||||
|
d = _dossier(doc_type="trackare", dp_selection=sel)
|
||||||
|
finalize_dp(d)
|
||||||
|
assert d.dp_trackare is not None
|
||||||
|
|
||||||
|
def test_trackare_detected_by_reason(self):
|
||||||
|
sel = _sel(code="K81.0", verdict="CONFIRMED", evidence=[],
|
||||||
|
reason="DP Trackare — source d'autorité")
|
||||||
|
d = _dossier(doc_type="crh", dp_selection=sel)
|
||||||
|
finalize_dp(d)
|
||||||
|
assert d.dp_trackare is not None
|
||||||
|
|
||||||
|
def test_trackare_detected_by_evidence(self):
|
||||||
|
sel = _sel(code="K81.0", verdict="CONFIRMED",
|
||||||
|
evidence=["Source: Trackare (codage établissement)"],
|
||||||
|
reason="DP")
|
||||||
|
d = _dossier(doc_type="crh", dp_selection=sel)
|
||||||
|
finalize_dp(d)
|
||||||
|
assert d.dp_trackare is not None
|
||||||
|
|
||||||
|
def test_crh_only_when_no_trackare_signal(self):
|
||||||
|
sel = _sel(code="K81.0", verdict="CONFIRMED",
|
||||||
|
evidence=["Diagnostic de sortie CRH"],
|
||||||
|
reason="CRH CONFIRMED")
|
||||||
|
d = _dossier(doc_type="crh", dp_selection=sel)
|
||||||
|
finalize_dp(d)
|
||||||
|
assert d.dp_trackare is None
|
||||||
|
assert d.dp_crh_only is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# finalize_dp — pas de dp_selection
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFinalizeDpNoSelection:
|
||||||
|
def test_no_dp_selection(self):
|
||||||
|
"""Dossier sans dp_selection → REVIEW, flag no_dp_source."""
|
||||||
|
d = _dossier(doc_type="crh")
|
||||||
|
finalize_dp(d)
|
||||||
|
assert d.dp_final is not None
|
||||||
|
assert d.dp_final.verdict == "REVIEW"
|
||||||
|
assert d.quality_flags.get("no_dp_source") is True
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# decide_dp_final — edge cases
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecideDpFinalEdgeCases:
|
||||||
|
def test_trackare_z_code_whitelisted(self):
|
||||||
|
"""Z03 (whitelisté) en Trackare-only → CONFIRMED."""
|
||||||
|
trackare = _sel(code="Z03.8", verdict="CONFIRMED",
|
||||||
|
evidence=["Source: Trackare"])
|
||||||
|
dp, flags, _ = decide_dp_final(trackare, None)
|
||||||
|
# Z03 is whitelisted
|
||||||
|
assert dp.verdict == "CONFIRMED"
|
||||||
|
|
||||||
|
def test_trackare_empty_evidence_gets_default(self):
|
||||||
|
"""Trackare-only sans evidence → evidence par défaut ajoutée."""
|
||||||
|
trackare = _sel(code="K81.0", verdict="CONFIRMED", evidence=[])
|
||||||
|
dp, flags, _ = decide_dp_final(trackare, None)
|
||||||
|
assert any("Trackare" in e for e in dp.evidence)
|
||||||
|
|
||||||
|
def test_both_sources_trackare_review_from_selector(self):
|
||||||
|
"""Si dp_selector a déjà mis REVIEW, le finalizer le respecte."""
|
||||||
|
trackare = _sel(code="K81.0", verdict="REVIEW",
|
||||||
|
evidence=["Source: Trackare"])
|
||||||
|
crh = _sel(code="K81.0", verdict="REVIEW",
|
||||||
|
evidence=["Diagnostic CRH"],
|
||||||
|
candidates=[_cand("K81.0")])
|
||||||
|
dp, flags, _ = decide_dp_final(trackare, crh)
|
||||||
|
# R2 corrobore → CONFIRMED
|
||||||
|
assert dp.verdict == "CONFIRMED"
|
||||||
|
assert flags.get("trackare_confirmed_by_crh") is True
|
||||||
|
|
||||||
|
def test_code_none_handling(self):
|
||||||
|
"""Trackare avec code None → pas de crash."""
|
||||||
|
trackare = _sel(code=None, verdict="REVIEW", evidence=["Source: Trackare"])
|
||||||
|
crh = _sel(code="K81.0", verdict="CONFIRMED",
|
||||||
|
evidence=["Diagnostic de sortie CRH"])
|
||||||
|
dp, flags, _ = decide_dp_final(trackare, crh)
|
||||||
|
# Code None ne commence pas par R, ne commence pas par Z
|
||||||
|
# Il ne matche pas CRH → R4 ambigu
|
||||||
|
assert dp is not None
|
||||||
Reference in New Issue
Block a user