Files
aivanov_CIM/tests/test_verificateur.py
2026-03-05 01:20:14 +01:00

663 lines
20 KiB
Python

"""
Tests pour le Vérificateur.
Ces tests vérifient que le Vérificateur:
- Utilise un prompt différent du Codeur (Exigence 4.1)
- Détecte les erreurs sensibles DIM (Exigences 4.2, 4.3, 4.4)
- Génère des vetos pour contradictions bloquantes (Exigence 4.5)
- Marque "à_revoir" pour contradictions non-bloquantes (Exigence 4.6)
- Fournit des alternatives (Exigence 4.6)
"""
import hashlib
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from pipeline_mco_pmsi.models.clinical import (
ClinicalFact,
Evidence,
Qualifier,
Span,
)
from pipeline_mco_pmsi.models.coding import (
Code,
CodingProposal,
DIMError,
)
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
from pipeline_mco_pmsi.verifiers.verificateur import Verificateur
@pytest.fixture
def mock_rag_engine():
"""Crée un mock du RAG Engine."""
return MagicMock()
@pytest.fixture
def verificateur(mock_rag_engine):
"""Crée une instance du Vérificateur avec un RAG Engine mocké."""
return Verificateur(
rag_engine=mock_rag_engine,
model_name="mock-llm",
model_version="1.0.0",
)
@pytest.fixture
def stay_metadata():
"""Crée des métadonnées de séjour pour les tests."""
return StayMetadata(
stay_id="stay_001",
admission_date=datetime(2024, 1, 1),
discharge_date=datetime(2024, 1, 5),
specialty="Chirurgie",
unit="Bloc opératoire",
age=45,
sex="M",
)
def create_evidence(doc_id="doc_001", start=100, end=120, text="Appendicite aiguë"):
"""Helper pour créer une preuve."""
return Evidence(
document_id=doc_id,
span=Span(start=start, end=end),
text=text,
context=f"Le patient présente {text}",
)
def create_qualifier(certainty="affirmé", markers=None, confidence=0.95):
"""Helper pour créer un qualificateur."""
return Qualifier(
certainty=certainty,
markers=markers or [],
confidence=confidence,
)
def create_fact(
fact_id="f_001",
fact_type="diagnostic",
text="Appendicite aiguë",
certainty="affirmé",
temporality="actuel",
evidence=None,
):
"""Helper pour créer un fait clinique."""
if evidence is None:
evidence = create_evidence(text=text)
return ClinicalFact(
fact_id=fact_id,
type=fact_type,
text=text,
qualifier=create_qualifier(certainty=certainty),
temporality=temporality,
evidence=evidence,
confidence=0.90,
)
def create_code(
code="K35.8",
label="Appendicite aiguë",
code_type="dp",
evidence=None,
confidence=0.85,
):
"""Helper pour créer un code."""
if evidence is None:
evidence = [create_evidence(text=label)]
return Code(
code=code,
label=label,
type=code_type,
evidence=evidence,
confidence=confidence,
reasoning=f"Code {code_type} proposé pour {label}",
referentiel_version="2026",
)
def create_proposal(
stay_id="stay_001",
dp=None,
dr=None,
das=None,
ccam=None,
prompt_version="codeur-1.0.0",
):
"""Helper pour créer une proposition de codage."""
model_version = ModelVersion(
model_name="mock-llm",
model_tag="1.0.0",
model_digest=hashlib.sha256(b"mock-llm:1.0.0").hexdigest(),
quantization=None,
)
return CodingProposal(
stay_id=stay_id,
dp=dp,
dr=dr,
das=das or [],
ccam=ccam or [],
reasoning="Proposition de codage basée sur les faits cliniques",
model_version=model_version,
prompt_version=prompt_version,
)
# ============================================================================
# Tests d'initialisation
# ============================================================================
def test_verificateur_initialization(verificateur):
"""Test l'initialisation du Vérificateur."""
assert verificateur.model_name == "mock-llm"
assert verificateur.model_version_str == "1.0.0"
assert verificateur.VERIFICATEUR_PROMPT_VERSION == "verificateur-1.0.0"
assert verificateur.model_digest is not None
def test_verificateur_prompt_version_different_from_codeur(verificateur):
"""
Test que le Vérificateur utilise un prompt différent du Codeur.
Exigence 4.1: Le Vérificateur DOIT utiliser un prompt différent du Codeur.
"""
codeur_prompt_version = "codeur-1.0.0"
assert verificateur.VERIFICATEUR_PROMPT_VERSION != codeur_prompt_version
# ============================================================================
# Tests de verify_proposal()
# ============================================================================
def test_verify_proposal_accepts_valid_proposal(verificateur):
"""
Test que le Vérificateur accepte une proposition valide.
Exigence 4.8: Quand le Vérificateur confirme la proposition,
le système doit accepter la proposition.
"""
# Créer une proposition valide
evidence = create_evidence()
fact = create_fact(evidence=evidence)
dp = create_code(evidence=[evidence])
proposal = create_proposal(dp=dp)
# Vérifier
result = verificateur.verify_proposal(proposal, [fact])
# Assertions
assert result.decision == "accept"
assert len(result.dim_errors) == 0
assert len(result.contradictions) == 0
assert result.stay_id == "stay_001"
assert result.prompt_version == "verificateur-1.0.0"
def test_verify_proposal_rejects_same_prompt_version(verificateur):
"""
Test que le Vérificateur rejette une proposition avec le même prompt.
Exigence 4.1: Le Vérificateur DOIT utiliser un prompt différent du Codeur.
"""
evidence = create_evidence()
fact = create_fact(evidence=evidence)
dp = create_code(evidence=[evidence])
# Créer une proposition avec le même prompt que le Vérificateur
proposal = create_proposal(
dp=dp,
prompt_version="verificateur-1.0.0" # MÊME prompt!
)
# Vérifier que cela lève une erreur
with pytest.raises(ValueError, match="DOIT utiliser un prompt différent"):
verificateur.verify_proposal(proposal, [fact])
# ============================================================================
# Tests de detect_dim_errors() - Diagnostics niés
# ============================================================================
def test_detect_dim_errors_negated_diagnostic(verificateur):
"""
Test la détection de diagnostic nié codé comme affirmé.
Exigence 4.2, 19.1: Le Vérificateur DOIT détecter les diagnostics niés
codés comme affirmés.
"""
# Créer un fait nié
evidence = create_evidence(text="Pas d'appendicite")
fact = create_fact(
text="Pas d'appendicite",
certainty="nié",
evidence=evidence,
)
# Créer un code basé sur ce fait nié
dp = create_code(
code="K35.8",
label="Appendicite",
evidence=[evidence],
)
proposal = create_proposal(dp=dp)
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact])
# Assertions
assert len(errors) == 1
assert errors[0].error_type == "negated_as_affirmed"
assert errors[0].severity == "bloquant"
assert "K35.8" in errors[0].affected_codes
def test_detect_dim_errors_suspected_as_dp(verificateur):
"""
Test la détection de diagnostic suspecté codé comme DP.
Exigence 19.2: Le Vérificateur DOIT détecter la transformation
de suspicion en certitude.
"""
# Créer un fait suspecté
evidence = create_evidence(text="Appendicite possible")
fact = create_fact(
text="Appendicite possible",
certainty="suspecté",
evidence=evidence,
)
# Créer un DP basé sur ce fait suspecté
dp = create_code(
code="K35.8",
label="Appendicite",
code_type="dp",
evidence=[evidence],
)
proposal = create_proposal(dp=dp)
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact])
# Assertions
assert len(errors) == 1
assert errors[0].error_type == "suspected_as_certain"
assert errors[0].severity == "bloquant"
# ============================================================================
# Tests de detect_dim_errors() - Antécédents
# ============================================================================
def test_detect_dim_errors_history_as_dp(verificateur):
"""
Test la détection d'antécédent codé comme DP.
Exigence 4.4, 19.4: Le Vérificateur DOIT détecter les antécédents
codés comme épisode actuel.
"""
# Créer un fait antécédent
evidence = create_evidence(text="Antécédent d'appendicectomie")
fact = create_fact(
text="Antécédent d'appendicectomie",
temporality="antecedent",
evidence=evidence,
)
# Créer un DP basé sur cet antécédent
dp = create_code(
code="Z90.4",
label="Absence d'appendice",
code_type="dp",
evidence=[evidence],
)
proposal = create_proposal(dp=dp)
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact])
# Assertions
assert len(errors) == 1
assert errors[0].error_type == "history_as_current"
assert errors[0].severity == "bloquant"
# ============================================================================
# Tests de detect_dim_errors() - Actes CCAM
# ============================================================================
def test_detect_dim_errors_ccam_without_evidence(verificateur):
"""
Test la détection d'acte CCAM sans preuve explicite.
Exigence 4.3, 19.3: Le Vérificateur DOIT détecter les actes CCAM
sans preuve explicite.
"""
# Créer un fait diagnostic (pas un acte)
evidence = create_evidence(text="Appendicite")
fact = create_fact(
fact_type="diagnostic", # Pas un acte!
text="Appendicite",
evidence=evidence,
)
# Créer un code CCAM basé sur ce fait diagnostic
ccam = create_code(
code="HHFA001",
label="Appendicectomie",
code_type="ccam",
evidence=[evidence],
)
proposal = create_proposal(ccam=[ccam])
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact])
# Assertions
assert len(errors) == 1
assert errors[0].error_type == "act_without_evidence"
assert errors[0].severity == "bloquant"
def test_detect_dim_errors_ccam_with_valid_evidence(verificateur):
"""
Test qu'un acte CCAM avec preuve valide n'est pas signalé.
"""
# Créer un fait acte
evidence = create_evidence(text="Appendicectomie réalisée")
fact = create_fact(
fact_type="acte", # Type correct
text="Appendicectomie réalisée",
evidence=evidence,
)
# Créer un code CCAM basé sur ce fait acte
ccam = create_code(
code="HHFA001",
label="Appendicectomie",
code_type="ccam",
evidence=[evidence],
)
proposal = create_proposal(ccam=[ccam])
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact])
# Assertions
assert len(errors) == 0
# ============================================================================
# Tests de detect_dim_errors() - Inversions DP/DAS
# ============================================================================
def test_detect_dim_errors_dp_das_inversion(verificateur):
"""
Test la détection d'inversion DP/DAS.
Exigence 19.5: Le Vérificateur DOIT détecter les inversions
grossières d'assignation DP/DAS.
"""
# Créer des faits
evidence_dp = create_evidence(text="Diabète")
fact_dp = create_fact(fact_id="f_001", text="Diabète", evidence=evidence_dp)
evidence_das = create_evidence(start=200, end=220, text="Appendicite aiguë")
fact_das = create_fact(fact_id="f_002", text="Appendicite aiguë", evidence=evidence_das)
# Créer un DP avec faible confiance et un DAS avec forte confiance
dp = create_code(
code="E11.9",
label="Diabète",
code_type="dp",
evidence=[evidence_dp],
confidence=0.60, # Faible
)
das = create_code(
code="K35.8",
label="Appendicite aiguë",
code_type="das",
evidence=[evidence_das],
confidence=0.95, # Forte (> DP + 0.1)
)
proposal = create_proposal(dp=dp, das=[das])
# Détecter les erreurs
errors = verificateur.detect_dim_errors(proposal, [fact_dp, fact_das])
# Assertions
assert len(errors) == 1
assert errors[0].error_type == "dp_das_inversion"
assert errors[0].severity == "a_revoir"
assert "E11.9" in errors[0].affected_codes
assert "K35.8" in errors[0].affected_codes
# ============================================================================
# Tests de décision (accept/veto/review)
# ============================================================================
def test_verify_proposal_veto_on_blocking_error(verificateur):
"""
Test que le Vérificateur génère un veto pour une erreur bloquante.
Exigence 4.5: Quand le Vérificateur détecte une contradiction bloquante,
le système doit opposer un veto.
"""
# Créer un fait nié
evidence = create_evidence(text="Pas d'appendicite")
fact = create_fact(
text="Pas d'appendicite",
certainty="nié",
evidence=evidence,
)
# Créer un code basé sur ce fait nié (erreur bloquante)
dp = create_code(evidence=[evidence])
proposal = create_proposal(dp=dp)
# Vérifier
result = verificateur.verify_proposal(proposal, [fact])
# Assertions
assert result.decision == "veto"
assert len(result.dim_errors) == 1
assert result.dim_errors[0].severity == "bloquant"
def test_verify_proposal_review_on_non_blocking_error(verificateur):
"""
Test que le Vérificateur marque "à_revoir" pour une erreur non-bloquante.
Exigence 4.6: Quand le Vérificateur détecte une contradiction non-bloquante,
le système doit marquer le code comme "à_revoir".
"""
# Créer des faits pour inversion DP/DAS
evidence_dp = create_evidence(text="Diabète")
fact_dp = create_fact(fact_id="f_001", text="Diabète", evidence=evidence_dp)
evidence_das = create_evidence(start=200, end=220, text="Appendicite")
fact_das = create_fact(fact_id="f_002", text="Appendicite", evidence=evidence_das)
# Créer une inversion DP/DAS (erreur non-bloquante)
dp = create_code(
code="E11.9",
label="Diabète",
code_type="dp",
evidence=[evidence_dp],
confidence=0.60,
)
das = create_code(
code="K35.8",
label="Appendicite",
code_type="das",
evidence=[evidence_das],
confidence=0.95,
)
proposal = create_proposal(dp=dp, das=[das])
# Vérifier
result = verificateur.verify_proposal(proposal, [fact_dp, fact_das])
# Assertions
assert result.decision == "review"
assert len(result.dim_errors) == 1
assert result.dim_errors[0].severity == "a_revoir"
# ============================================================================
# Tests d'alternatives
# ============================================================================
def test_verify_proposal_provides_alternatives(verificateur):
"""
Test que le Vérificateur fournit des alternatives.
Exigence 4.6: Le Vérificateur doit fournir des alternatives.
"""
# Créer un fait nié pour le DP
evidence_dp = create_evidence(text="Pas d'appendicite")
fact_dp = create_fact(
fact_id="f_001",
text="Pas d'appendicite",
certainty="nié",
evidence=evidence_dp,
)
# Créer un fait valide pour le DAS
evidence_das = create_evidence(start=200, end=220, text="Gastrite")
fact_das = create_fact(
fact_id="f_002",
text="Gastrite",
evidence=evidence_das,
)
# Créer une proposition avec DP invalide et DAS valide
dp = create_code(
code="K35.8",
label="Appendicite",
code_type="dp",
evidence=[evidence_dp],
)
das = create_code(
code="K29.7",
label="Gastrite",
code_type="das",
evidence=[evidence_das],
confidence=0.90,
)
proposal = create_proposal(dp=dp, das=[das])
# Vérifier
result = verificateur.verify_proposal(proposal, [fact_dp, fact_das])
# Assertions
assert result.decision == "veto"
assert len(result.alternatives) > 0
# L'alternative devrait être le DAS comme nouveau DP
assert result.alternatives[0].code == "K29.7"
assert result.alternatives[0].type == "dp"
# ============================================================================
# Tests de raisonnement
# ============================================================================
def test_verify_proposal_generates_reasoning(verificateur):
"""
Test que le Vérificateur génère un raisonnement détaillé.
"""
evidence = create_evidence()
fact = create_fact(evidence=evidence)
dp = create_code(evidence=[evidence])
proposal = create_proposal(dp=dp)
# Vérifier
result = verificateur.verify_proposal(proposal, [fact])
# Assertions
assert result.reasoning is not None
assert len(result.reasoning) > 0
assert "Vérification indépendante" in result.reasoning
assert "verificateur-1.0.0" in result.reasoning
assert "codeur-1.0.0" in result.reasoning
# ============================================================================
# Tests de cas complexes
# ============================================================================
def test_verify_proposal_multiple_errors(verificateur):
"""
Test la détection de plusieurs erreurs simultanées.
"""
# Créer plusieurs faits avec erreurs
evidence1 = create_evidence(text="Pas d'appendicite")
fact1 = create_fact(
fact_id="f_001",
text="Pas d'appendicite",
certainty="nié",
evidence=evidence1,
)
evidence2 = create_evidence(start=200, end=220, text="Diabète ancien")
fact2 = create_fact(
fact_id="f_002",
text="Diabète ancien",
temporality="antecedent",
evidence=evidence2,
)
# Créer des codes avec erreurs
dp = create_code(
code="K35.8",
label="Appendicite",
code_type="dp",
evidence=[evidence1],
)
das = create_code(
code="E11.9",
label="Diabète",
code_type="das",
evidence=[evidence2],
)
proposal = create_proposal(dp=dp, das=[das])
# Vérifier
result = verificateur.verify_proposal(proposal, [fact1, fact2])
# Assertions
assert result.decision == "veto"
assert len(result.dim_errors) >= 1 # Au moins l'erreur du DP nié
def test_verify_proposal_no_errors_with_das_only(verificateur):
"""
Test qu'une proposition sans DP mais avec DAS valides est acceptée.
"""
evidence = create_evidence(text="Diabète")
fact = create_fact(text="Diabète", evidence=evidence)
das = create_code(
code="E11.9",
label="Diabète",
code_type="das",
evidence=[evidence],
)
proposal = create_proposal(dp=None, das=[das])
# Vérifier
result = verificateur.verify_proposal(proposal, [fact])
# Assertions
assert result.decision == "accept"
assert len(result.dim_errors) == 0