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