Initial commit
This commit is contained in:
382
tests/test_rules_integration.py
Normal file
382
tests/test_rules_integration.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user