Initial commit

This commit is contained in:
Dom
2026-03-05 01:20:14 +01:00
commit 2163e574c1
184 changed files with 354881 additions and 0 deletions

View File

@@ -0,0 +1,532 @@
"""
Tests unitaires pour le GroupageValidator.
Ces tests vérifient:
- L'intégration de la fonction de groupage ATIH (mock pour POC)
- La transformation CIM-10/CCAM → GHM/GHS
- La vérification des dates de réalisation CCAM (erreur bloquante 2026)
- La vérification de la version FG
- L'enregistrement de la version FG dans l'audit
"""
import uuid
from datetime import datetime
import pytest
from pipeline_mco_pmsi.models.clinical import ClinicalFact, Evidence, Qualifier, Span
from pipeline_mco_pmsi.models.coding import Code, CodingProposal
from pipeline_mco_pmsi.models.metadata import ModelVersion, StayMetadata
from pipeline_mco_pmsi.validators.groupage_validator import GroupageValidator
@pytest.fixture
def stay_metadata():
"""Fixture pour les métadonnées de séjour."""
return StayMetadata(
stay_id="stay_001",
admission_date=datetime(2026, 1, 15),
discharge_date=datetime(2026, 1, 20),
age=45,
sex="M",
specialty="Chirurgie",
)
@pytest.fixture
def model_version():
"""Fixture pour la version du modèle."""
return ModelVersion(
model_name="test-model",
model_tag="v1.0",
model_digest="a" * 64, # SHA-256 hash (64 caractères hexadécimaux)
quantization="q4_0",
)
@pytest.fixture
def sample_evidence():
"""Fixture pour une preuve."""
return Evidence(
document_id="doc_001",
span=Span(start=100, end=120),
text="Gastrite aiguë",
context="Le patient présente une gastrite aiguë confirmée par endoscopie.",
)
@pytest.fixture
def sample_dp_code(sample_evidence):
"""Fixture pour un code DP."""
return Code(
code="K29.1",
label="Gastrite aiguë",
type="dp",
evidence=[sample_evidence],
confidence=0.9,
reasoning="Diagnostic principal basé sur l'endoscopie",
referentiel_version="2026",
)
@pytest.fixture
def sample_das_code(sample_evidence):
"""Fixture pour un code DAS."""
return Code(
code="E11.9",
label="Diabète de type 2",
type="das",
evidence=[sample_evidence],
confidence=0.85,
reasoning="Antécédent de diabète mentionné",
referentiel_version="2026",
)
@pytest.fixture
def sample_ccam_code_with_date(sample_evidence):
"""Fixture pour un code CCAM avec date."""
evidence_with_date = Evidence(
document_id="doc_001",
span=Span(start=200, end=250),
text="Endoscopie réalisée le 15/01/2026",
context="Endoscopie digestive haute réalisée le 15/01/2026 sous anesthésie.",
)
return Code(
code="HGQE002",
label="Endoscopie œsogastroduodénale",
type="ccam",
evidence=[evidence_with_date],
confidence=0.95,
reasoning="Acte réalisé le 15/01/2026 selon le compte rendu opératoire",
referentiel_version="2026",
)
@pytest.fixture
def sample_ccam_code_without_date(sample_evidence):
"""Fixture pour un code CCAM sans date."""
return Code(
code="YYYY001",
label="Acte sans date",
type="ccam",
evidence=[sample_evidence],
confidence=0.8,
reasoning="Acte mentionné dans le dossier",
referentiel_version="2026",
)
@pytest.fixture
def coding_proposal_complete(
sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version
):
"""Fixture pour une proposition de codage complète."""
return CodingProposal(
stay_id="stay_001",
dp=sample_dp_code,
dr=None,
das=[sample_das_code],
ccam=[sample_ccam_code_with_date],
reasoning="Codage basé sur les documents cliniques",
model_version=model_version,
prompt_version="v1.0",
)
@pytest.fixture
def coding_proposal_missing_ccam_date(
sample_dp_code, sample_das_code, sample_ccam_code_without_date, model_version
):
"""Fixture pour une proposition avec CCAM sans date."""
return CodingProposal(
stay_id="stay_001",
dp=sample_dp_code,
dr=None,
das=[sample_das_code],
ccam=[sample_ccam_code_without_date],
reasoning="Codage basé sur les documents cliniques",
model_version=model_version,
prompt_version="v1.0",
)
@pytest.fixture
def coding_proposal_no_dp(sample_das_code, sample_ccam_code_with_date, model_version):
"""Fixture pour une proposition sans DP."""
return CodingProposal(
stay_id="stay_001",
dp=None,
dr=None,
das=[sample_das_code],
ccam=[sample_ccam_code_with_date],
reasoning="Codage incomplet",
model_version=model_version,
prompt_version="v1.0",
)
class TestGroupageValidatorInitialization:
"""Tests d'initialisation du GroupageValidator."""
def test_init_default_version(self):
"""Test l'initialisation avec la version par défaut."""
validator = GroupageValidator()
assert validator.groupage_version == "2026"
def test_init_custom_version(self):
"""Test l'initialisation avec une version personnalisée."""
validator = GroupageValidator(groupage_version="2025")
assert validator.groupage_version == "2025"
def test_get_version_info(self):
"""Test la récupération des informations de version."""
validator = GroupageValidator(groupage_version="2026")
version_info = validator.get_version_info()
assert version_info["groupage_version"] == "2026"
assert version_info["implementation"] == "mock_poc"
assert "library_version" in version_info
class TestCheckCCAMDates:
"""Tests de la vérification des dates CCAM."""
def test_ccam_with_date_in_reasoning(self, sample_ccam_code_with_date):
"""Test qu'un code CCAM avec date dans le reasoning est accepté."""
validator = GroupageValidator()
missing_dates = validator.check_ccam_dates([sample_ccam_code_with_date])
assert len(missing_dates) == 0
def test_ccam_without_date(self, sample_ccam_code_without_date):
"""Test qu'un code CCAM sans date est détecté."""
validator = GroupageValidator()
missing_dates = validator.check_ccam_dates([sample_ccam_code_without_date])
assert len(missing_dates) == 1
assert missing_dates[0] == "YYYY001"
def test_multiple_ccam_mixed_dates(
self, sample_ccam_code_with_date, sample_ccam_code_without_date
):
"""Test avec plusieurs codes CCAM, certains avec date, d'autres sans."""
validator = GroupageValidator()
missing_dates = validator.check_ccam_dates(
[sample_ccam_code_with_date, sample_ccam_code_without_date]
)
assert len(missing_dates) == 1
assert missing_dates[0] == "YYYY001"
def test_empty_ccam_list(self):
"""Test avec une liste vide de codes CCAM."""
validator = GroupageValidator()
missing_dates = validator.check_ccam_dates([])
assert len(missing_dates) == 0
def test_ccam_with_date_in_evidence(self, sample_evidence):
"""Test qu'un code CCAM avec date dans les preuves est accepté."""
evidence_with_date = Evidence(
document_id="doc_001",
span=Span(start=200, end=250),
text="Intervention du 15/01/2026",
context="Chirurgie effectuée le 15/01/2026.",
)
ccam_code = Code(
code="HGQE002",
label="Endoscopie",
type="ccam",
evidence=[evidence_with_date],
confidence=0.95,
reasoning="Acte chirurgical",
referentiel_version="2026",
)
validator = GroupageValidator()
missing_dates = validator.check_ccam_dates([ccam_code])
assert len(missing_dates) == 0
class TestValidateGroupage:
"""Tests de la validation de groupage complète."""
def test_validate_groupage_success(self, coding_proposal_complete, stay_metadata):
"""Test une validation de groupage réussie."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_complete, stay_metadata)
assert result.stay_id == "stay_001"
assert result.ghm is not None
assert result.ghs is not None
assert len(result.ccam_date_errors) == 0
assert result.groupage_version == "2026"
assert isinstance(result.groupage_date, datetime)
def test_validate_groupage_missing_ccam_date(
self, coding_proposal_missing_ccam_date, stay_metadata
):
"""Test la détection d'une date CCAM manquante."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(
coding_proposal_missing_ccam_date, stay_metadata
)
assert result.stay_id == "stay_001"
assert len(result.ccam_date_errors) == 1
assert result.ccam_date_errors[0] == "YYYY001"
# Vérifier qu'une erreur bloquante est générée
blocking_errors = [
err for err in result.groupage_errors if err.severity == "bloquant"
]
assert len(blocking_errors) >= 1
assert any("Date de réalisation manquante" in err.message for err in blocking_errors)
def test_validate_groupage_no_dp(self, coding_proposal_no_dp, stay_metadata):
"""Test la détection d'un DP manquant."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_no_dp, stay_metadata)
assert result.stay_id == "stay_001"
assert result.ghm is None
assert result.ghs is None
# Vérifier qu'une erreur bloquante est générée
blocking_errors = [
err for err in result.groupage_errors if err.severity == "bloquant"
]
assert len(blocking_errors) >= 1
assert any("Aucun Diagnostic Principal" in err.message for err in blocking_errors)
def test_validate_groupage_version_mismatch(
self, coding_proposal_complete, stay_metadata
):
"""Test la détection d'une version FG incorrecte."""
# Créer un validateur avec une version différente de l'année du séjour
validator = GroupageValidator(groupage_version="2025")
with pytest.raises(ValueError) as exc_info:
validator.validate_groupage(coding_proposal_complete, stay_metadata)
assert "Version FG" in str(exc_info.value)
assert "ne correspond pas" in str(exc_info.value)
def test_validate_groupage_many_das(
self, sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version, stay_metadata
):
"""Test avec un nombre élevé de DAS."""
# Créer 25 DAS (au-dessus de la limite de 20)
many_das = [sample_das_code] * 25
proposal = CodingProposal(
stay_id="stay_001",
dp=sample_dp_code,
dr=None,
das=many_das,
ccam=[sample_ccam_code_with_date],
reasoning="Codage avec beaucoup de DAS",
model_version=model_version,
prompt_version="v1.0",
)
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(proposal, stay_metadata)
# Vérifier qu'un avertissement est généré
review_errors = [
err for err in result.groupage_errors if err.severity == "a_revoir"
]
assert len(review_errors) >= 1
assert any("Nombre élevé de DAS" in err.message for err in review_errors)
def test_validate_groupage_many_ccam(
self, sample_dp_code, sample_das_code, sample_ccam_code_with_date, model_version, stay_metadata
):
"""Test avec un nombre élevé d'actes CCAM."""
# Créer 35 actes CCAM (au-dessus de la limite de 30)
many_ccam = [sample_ccam_code_with_date] * 35
proposal = CodingProposal(
stay_id="stay_001",
dp=sample_dp_code,
dr=None,
das=[sample_das_code],
ccam=many_ccam,
reasoning="Codage avec beaucoup d'actes",
model_version=model_version,
prompt_version="v1.0",
)
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(proposal, stay_metadata)
# Vérifier qu'un avertissement est généré
review_errors = [
err for err in result.groupage_errors if err.severity == "a_revoir"
]
assert len(review_errors) >= 1
assert any("Nombre élevé d'actes CCAM" in err.message for err in review_errors)
class TestGHMGHSGeneration:
"""Tests de la génération de GHM/GHS (mock)."""
def test_ghm_format(self, coding_proposal_complete, stay_metadata):
"""Test que le GHM généré a le bon format."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_complete, stay_metadata)
# Format GHM: 2 chiffres + 1 lettre + 2 chiffres (ex: "05K02")
assert result.ghm is not None
assert len(result.ghm) == 5
assert result.ghm[:2].isdigit()
assert result.ghm[2].isalpha()
assert result.ghm[3:].isdigit()
def test_ghs_generated(self, coding_proposal_complete, stay_metadata):
"""Test que le GHS est généré."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_complete, stay_metadata)
assert result.ghs is not None
assert len(result.ghs) > 0
def test_ghm_deterministic(self, coding_proposal_complete, stay_metadata):
"""Test que le GHM est déterministe pour les mêmes codes."""
validator = GroupageValidator(groupage_version="2026")
result1 = validator.validate_groupage(coding_proposal_complete, stay_metadata)
result2 = validator.validate_groupage(coding_proposal_complete, stay_metadata)
# Le GHM devrait être identique pour les mêmes codes
assert result1.ghm == result2.ghm
assert result1.ghs == result2.ghs
class TestVersionVerification:
"""Tests de la vérification de version FG."""
def test_version_matches_year(self, coding_proposal_complete):
"""Test que la version FG correspond à l'année du séjour."""
stay_metadata = StayMetadata(
stay_id="stay_001",
admission_date=datetime(2026, 1, 15),
discharge_date=datetime(2026, 1, 20),
age=45,
sex="M",
specialty="Chirurgie",
)
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_complete, stay_metadata)
# Pas d'exception levée, la version correspond
assert result.groupage_version == "2026"
def test_version_mismatch_raises_error(self, coding_proposal_complete):
"""Test qu'une version FG incorrecte lève une erreur."""
stay_metadata = StayMetadata(
stay_id="stay_001",
admission_date=datetime(2026, 1, 15),
discharge_date=datetime(2026, 1, 20),
age=45,
sex="M",
specialty="Chirurgie",
)
validator = GroupageValidator(groupage_version="2025")
with pytest.raises(ValueError) as exc_info:
validator.validate_groupage(coding_proposal_complete, stay_metadata)
assert "2025" in str(exc_info.value)
assert "2026" in str(exc_info.value)
def test_version_recorded_in_result(self, coding_proposal_complete, stay_metadata):
"""Test que la version FG est enregistrée dans le résultat."""
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(coding_proposal_complete, stay_metadata)
assert result.groupage_version == "2026"
assert isinstance(result.groupage_date, datetime)
class TestEdgeCases:
"""Tests des cas limites."""
def test_empty_proposal(self, model_version, stay_metadata):
"""Test avec une proposition vide."""
proposal = CodingProposal(
stay_id="stay_001",
dp=None,
dr=None,
das=[],
ccam=[],
reasoning="Proposition vide",
model_version=model_version,
prompt_version="v1.0",
)
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(proposal, stay_metadata)
# Devrait générer une erreur bloquante pour DP manquant
assert result.ghm is None
assert result.ghs is None
blocking_errors = [
err for err in result.groupage_errors if err.severity == "bloquant"
]
assert len(blocking_errors) >= 1
def test_only_dp(self, sample_dp_code, model_version, stay_metadata):
"""Test avec seulement un DP."""
proposal = CodingProposal(
stay_id="stay_001",
dp=sample_dp_code,
dr=None,
das=[],
ccam=[],
reasoning="Seulement DP",
model_version=model_version,
prompt_version="v1.0",
)
validator = GroupageValidator(groupage_version="2026")
result = validator.validate_groupage(proposal, stay_metadata)
# Devrait réussir avec seulement un DP
assert result.ghm is not None
assert result.ghs is not None
assert len(result.ccam_date_errors) == 0
def test_multiple_ccam_some_without_dates(
self, sample_ccam_code_with_date, sample_ccam_code_without_date
):
"""Test avec plusieurs codes CCAM, certains sans dates."""
validator = GroupageValidator()
# Créer un deuxième code sans date
ccam_without_date_2 = Code(
code="ZZZZ999",
label="Autre acte sans date",
type="ccam",
evidence=[sample_ccam_code_without_date.evidence[0]],
confidence=0.7,
reasoning="Acte mentionné",
referentiel_version="2026",
)
missing_dates = validator.check_ccam_dates(
[
sample_ccam_code_with_date,
sample_ccam_code_without_date,
ccam_without_date_2,
]
)
assert len(missing_dates) == 2
assert "YYYY001" in missing_dates
assert "ZZZZ999" in missing_dates