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

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"])