383 lines
9.9 KiB
Python
383 lines
9.9 KiB
Python
"""
|
|
Tests d'intégration pour le système de règles configurables.
|
|
|
|
Ce module teste l'intégration du RulesManager avec le PMSIValidator
|
|
et le Pipeline pour appliquer des règles spécifiques à l'établissement.
|
|
|
|
Exigences: 20.4
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
from pipeline_mco_pmsi.models.clinical import (
|
|
ClinicalDocument,
|
|
ClinicalFact,
|
|
Evidence,
|
|
Qualifier,
|
|
Span,
|
|
StructuredStay,
|
|
)
|
|
from pipeline_mco_pmsi.models.coding import Code, CodingProposal
|
|
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
|
|
from pipeline_mco_pmsi.rag.rag_engine import RAGEngine
|
|
from pipeline_mco_pmsi.rules.rules_manager import RulesManager
|
|
from pipeline_mco_pmsi.validators.pmsi_validator import PMSIValidator
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_rag_engine():
|
|
"""Crée un mock du RAG Engine."""
|
|
engine = MagicMock(spec=RAGEngine)
|
|
engine.retrieve_eligibility_criteria.return_value = None
|
|
return engine
|
|
|
|
|
|
@pytest.fixture
|
|
def model_version():
|
|
"""Crée une version de modèle de test."""
|
|
return ModelVersion(
|
|
model_name="test-model",
|
|
model_tag="v1.0",
|
|
model_digest="a" * 64,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def rules_manager_with_default_rules():
|
|
"""Crée un RulesManager avec les règles par défaut."""
|
|
manager = RulesManager()
|
|
manager.create_default_ruleset()
|
|
return manager
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_stay():
|
|
"""Crée un séjour de test."""
|
|
doc = ClinicalDocument(
|
|
document_id="doc1",
|
|
document_type="cr_operatoire",
|
|
content="Patient avec diabète de type 2.",
|
|
metadata={},
|
|
creation_date=datetime.now(),
|
|
priority=1,
|
|
)
|
|
|
|
fact = ClinicalFact(
|
|
fact_id="fact1",
|
|
type="diagnostic",
|
|
text="diabète de type 2",
|
|
qualifier=Qualifier(
|
|
certainty="affirmé",
|
|
negation=False,
|
|
temporality="actuel",
|
|
confidence=0.9,
|
|
),
|
|
evidence=Evidence(
|
|
document_id="doc1",
|
|
span=Span(start=13, end=30),
|
|
text="diabète de type 2",
|
|
),
|
|
temporality="actuel",
|
|
confidence=0.9,
|
|
)
|
|
|
|
return StructuredStay(
|
|
stay_id="stay1",
|
|
documents=[doc],
|
|
sections=[],
|
|
facts=[fact],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_proposal(model_version):
|
|
"""Crée une proposition de codage de test."""
|
|
dp = Code(
|
|
code="E11.9",
|
|
type="dp",
|
|
label="Diabète sucré de type 2, sans complication",
|
|
confidence=0.9,
|
|
reasoning="Diabète de type 2 mentionné dans le CRO",
|
|
evidence=[
|
|
Evidence(
|
|
document_id="doc1",
|
|
span=Span(start=13, end=30),
|
|
text="diabète de type 2",
|
|
)
|
|
],
|
|
referentiel_version="2026",
|
|
)
|
|
|
|
return CodingProposal(
|
|
stay_id="stay1",
|
|
dp=dp,
|
|
dr=None,
|
|
das=[],
|
|
ccam=[],
|
|
reasoning="Proposition de test",
|
|
model_version=model_version,
|
|
prompt_version="test-prompt-1.0",
|
|
)
|
|
|
|
|
|
def test_pmsi_validator_without_rules_manager(mock_rag_engine, sample_proposal, sample_stay):
|
|
"""
|
|
Test que le PMSIValidator fonctionne sans RulesManager.
|
|
|
|
Exigences: 20.4
|
|
"""
|
|
validator = PMSIValidator(rag_engine=mock_rag_engine)
|
|
|
|
issues = validator.validate_proposal(sample_proposal, sample_stay)
|
|
|
|
# Devrait fonctionner sans erreur
|
|
assert isinstance(issues, list)
|
|
|
|
|
|
def test_pmsi_validator_with_rules_manager(
|
|
mock_rag_engine,
|
|
rules_manager_with_default_rules,
|
|
sample_proposal,
|
|
sample_stay,
|
|
):
|
|
"""
|
|
Test que le PMSIValidator applique les règles du RulesManager.
|
|
|
|
Exigences: 20.4
|
|
"""
|
|
validator = PMSIValidator(
|
|
rag_engine=mock_rag_engine,
|
|
rules_manager=rules_manager_with_default_rules,
|
|
)
|
|
|
|
issues = validator.validate_proposal(sample_proposal, sample_stay)
|
|
|
|
# Devrait fonctionner et appliquer les règles
|
|
assert isinstance(issues, list)
|
|
|
|
|
|
def test_rule_dp_with_evidence_applied(
|
|
mock_rag_engine,
|
|
rules_manager_with_default_rules,
|
|
model_version,
|
|
sample_stay,
|
|
):
|
|
"""
|
|
Test que la règle 'DP avec preuves' est appliquée.
|
|
|
|
Exigences: 20.4
|
|
"""
|
|
# Créer une proposition avec DP qui a une preuve minimale
|
|
# (Code requires at least 1 evidence)
|
|
dp_with_minimal_evidence = Code(
|
|
code="E11.9",
|
|
type="dp",
|
|
label="Diabète sucré de type 2",
|
|
confidence=0.9,
|
|
reasoning="Test",
|
|
evidence=[
|
|
Evidence(
|
|
document_id="doc1",
|
|
span=Span(start=0, end=1),
|
|
text="x", # Preuve minimale
|
|
)
|
|
],
|
|
referentiel_version="2026",
|
|
)
|
|
|
|
proposal = CodingProposal(
|
|
stay_id="stay1",
|
|
dp=dp_with_minimal_evidence,
|
|
dr=None,
|
|
das=[],
|
|
ccam=[],
|
|
reasoning="Test",
|
|
model_version=model_version,
|
|
prompt_version="test-prompt-1.0",
|
|
)
|
|
|
|
validator = PMSIValidator(
|
|
rag_engine=mock_rag_engine,
|
|
rules_manager=rules_manager_with_default_rules,
|
|
)
|
|
|
|
issues = validator.validate_proposal(proposal, sample_stay)
|
|
|
|
# Devrait valider sans erreur (a au moins une preuve)
|
|
assert isinstance(issues, list)
|
|
|
|
|
|
def test_rule_negated_fact_rejected(
|
|
mock_rag_engine,
|
|
rules_manager_with_default_rules,
|
|
model_version,
|
|
):
|
|
"""
|
|
Test que la règle 'Pas de codes pour faits niés' est appliquée.
|
|
|
|
Exigences: 20.4
|
|
"""
|
|
# Créer un fait nié
|
|
negated_fact = ClinicalFact(
|
|
fact_id="fact1",
|
|
type="diagnostic",
|
|
text="diabète",
|
|
qualifier=Qualifier(
|
|
certainty="nié",
|
|
negation=True,
|
|
temporality="actuel",
|
|
confidence=0.9,
|
|
),
|
|
evidence=Evidence(
|
|
document_id="doc1",
|
|
span=Span(start=0, end=7),
|
|
text="diabète",
|
|
),
|
|
temporality="actuel",
|
|
confidence=0.9,
|
|
)
|
|
|
|
stay = StructuredStay(
|
|
stay_id="stay1",
|
|
documents=[
|
|
ClinicalDocument(
|
|
document_id="doc1",
|
|
document_type="cr_operatoire",
|
|
content="Pas de diabète.",
|
|
metadata={},
|
|
creation_date=datetime.now(),
|
|
priority=1,
|
|
)
|
|
],
|
|
sections=[],
|
|
facts=[negated_fact],
|
|
)
|
|
|
|
# Créer une proposition qui code le fait nié
|
|
dp = Code(
|
|
code="E11.9",
|
|
type="dp",
|
|
label="Diabète",
|
|
confidence=0.9,
|
|
reasoning="Test",
|
|
evidence=[
|
|
Evidence(
|
|
document_id="doc1",
|
|
span=Span(start=0, end=7),
|
|
text="diabète",
|
|
)
|
|
],
|
|
referentiel_version="2026",
|
|
)
|
|
|
|
proposal = CodingProposal(
|
|
stay_id="stay1",
|
|
dp=dp,
|
|
dr=None,
|
|
das=[],
|
|
ccam=[],
|
|
reasoning="Test",
|
|
model_version=model_version,
|
|
prompt_version="test-prompt-1.0",
|
|
)
|
|
|
|
validator = PMSIValidator(
|
|
rag_engine=mock_rag_engine,
|
|
rules_manager=rules_manager_with_default_rules,
|
|
)
|
|
|
|
issues = validator.validate_proposal(proposal, stay)
|
|
|
|
# Devrait détecter l'erreur zéro-tolérance
|
|
blocking_issues = [i for i in issues if i.severity == "bloquant"]
|
|
assert len(blocking_issues) > 0
|
|
|
|
|
|
def test_conservative_mode_rules(
|
|
mock_rag_engine,
|
|
sample_stay,
|
|
):
|
|
"""
|
|
Test que le mode conservateur applique des règles strictes.
|
|
|
|
Exigences: 20.3
|
|
"""
|
|
# Créer un RulesManager en mode conservateur
|
|
manager = RulesManager()
|
|
ruleset = manager.create_default_ruleset()
|
|
assert ruleset.mode == "conservateur"
|
|
|
|
validator = PMSIValidator(
|
|
rag_engine=mock_rag_engine,
|
|
rules_manager=manager,
|
|
)
|
|
|
|
# Le mode conservateur devrait être actif
|
|
assert manager.is_conservative_mode()
|
|
assert not manager.is_aggressive_mode()
|
|
|
|
|
|
def test_rules_version_tracking(rules_manager_with_default_rules):
|
|
"""
|
|
Test que la version et le hash des règles sont trackés.
|
|
|
|
Exigences: 20.2
|
|
"""
|
|
version_info = rules_manager_with_default_rules.get_version_info()
|
|
|
|
assert "version" in version_info
|
|
assert "hash" in version_info
|
|
assert "mode" in version_info
|
|
assert "rules_count" in version_info
|
|
|
|
# Le hash doit être un SHA-256 (64 caractères hex)
|
|
assert len(version_info["hash"]) == 64
|
|
assert all(c in "0123456789abcdef" for c in version_info["hash"])
|
|
|
|
|
|
def test_rules_by_category(rules_manager_with_default_rules):
|
|
"""
|
|
Test la récupération de règles par catégorie.
|
|
|
|
Exigences: 20.1
|
|
"""
|
|
# Récupérer les règles DP
|
|
dp_rules = rules_manager_with_default_rules.get_rules_by_category("dp")
|
|
assert len(dp_rules) > 0
|
|
assert all(rule.category == "dp" for rule in dp_rules)
|
|
|
|
# Récupérer les règles de validation
|
|
validation_rules = rules_manager_with_default_rules.get_rules_by_category("validation")
|
|
assert len(validation_rules) > 0
|
|
assert all(rule.category == "validation" for rule in validation_rules)
|
|
|
|
# Récupérer les règles CCAM
|
|
ccam_rules = rules_manager_with_default_rules.get_rules_by_category("ccam")
|
|
assert len(ccam_rules) > 0
|
|
assert all(rule.category == "ccam" for rule in ccam_rules)
|
|
|
|
|
|
def test_rule_by_id(rules_manager_with_default_rules):
|
|
"""
|
|
Test la récupération d'une règle par son ID.
|
|
|
|
Exigences: 20.1
|
|
"""
|
|
# Récupérer une règle spécifique
|
|
rule = rules_manager_with_default_rules.get_rule_by_id("dp_001")
|
|
|
|
assert rule is not None
|
|
assert rule.rule_id == "dp_001"
|
|
assert rule.name == "DP obligatoire"
|
|
|
|
# Règle inexistante
|
|
nonexistent = rules_manager_with_default_rules.get_rule_by_id("nonexistent")
|
|
assert nonexistent is None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|