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

553 lines
17 KiB
Python

"""
Tests unitaires pour le QuestionGenerator.
Ces tests vérifient:
- La génération de questions priorisées (max 5)
- La détection d'incohérences codes/faits
- La priorisation des questions par impact
"""
from datetime import datetime
import pytest
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
from pipeline_mco_pmsi.models.validation import ValidationIssue
from pipeline_mco_pmsi.validators.question_generator import QuestionGenerator
@pytest.fixture
def question_generator():
"""Crée une instance de QuestionGenerator."""
return QuestionGenerator()
@pytest.fixture
def sample_document():
"""Crée un document clinique de test."""
return ClinicalDocument(
document_id="doc_001",
document_type="cr_medical",
content="Patient présente une gastrite aiguë confirmée par endoscopie.",
creation_date=datetime(2024, 1, 15, 10, 30),
author="Dr. Martin",
priority=2,
)
@pytest.fixture
def sample_evidence():
"""Crée une preuve de test."""
return Evidence(
document_id="doc_001",
span=Span(start=20, end=35),
text="gastrite aiguë",
context="Patient présente une gastrite aiguë confirmée",
)
@pytest.fixture
def sample_code(sample_evidence):
"""Crée un code de test."""
return Code(
code="K29.1",
label="Gastrite aiguë",
type="dp",
evidence=[sample_evidence],
confidence=0.85,
reasoning="Diagnostic principal confirmé par endoscopie",
referentiel_version="2026",
)
@pytest.fixture
def sample_proposal(sample_code):
"""Crée une proposition de codage de test."""
return CodingProposal(
stay_id="stay_001",
dp=sample_code,
dr=None,
das=[],
ccam=[],
reasoning="Séjour pour gastrite aiguë",
model_version=ModelVersion(
model_name="test-model",
model_tag="v1.0",
model_digest="a" * 64,
),
prompt_version="v1.0",
)
@pytest.fixture
def sample_fact(sample_evidence):
"""Crée un fait clinique de test."""
return ClinicalFact(
fact_id="fact_001",
type="diagnostic",
text="gastrite aiguë",
qualifier=Qualifier(
certainty="affirmé",
markers=[],
confidence=0.9,
),
temporality="actuel",
evidence=sample_evidence,
confidence=0.9,
)
@pytest.fixture
def sample_stay(sample_document, sample_fact):
"""Crée un séjour structuré de test."""
return StructuredStay(
stay_id="stay_001",
documents=[sample_document],
sections=[],
facts=[sample_fact],
)
class TestQuestionGeneratorBasic:
"""Tests de base pour le QuestionGenerator."""
def test_initialization(self):
"""Test l'initialisation du générateur."""
generator = QuestionGenerator()
assert generator.MAX_QUESTIONS == 5
def test_generate_questions_returns_list(
self, question_generator, sample_proposal, sample_stay
):
"""Test que generate_questions retourne une liste."""
questions = question_generator.generate_questions(
sample_proposal, sample_stay, []
)
assert isinstance(questions, list)
def test_generate_questions_respects_max_limit(
self, question_generator, sample_proposal, sample_stay
):
"""Test que le nombre de questions ne dépasse pas MAX_QUESTIONS."""
# Créer beaucoup de problèmes de validation
many_issues = [
ValidationIssue(
issue_id=f"i{i}",
severity="bloquant",
category="missing_info",
message=f"Problème {i}",
affected_codes=[],
suggested_action=f"Action {i}",
)
for i in range(10)
]
questions = question_generator.generate_questions(
sample_proposal, sample_stay, many_issues
)
assert len(questions) <= QuestionGenerator.MAX_QUESTIONS
class TestQuestionGeneration:
"""Tests pour la génération de questions."""
def test_generate_from_blocking_issues(
self, question_generator, sample_proposal, sample_stay
):
"""Test la génération de questions depuis des problèmes bloquants."""
blocking_issue = ValidationIssue(
issue_id="i1",
severity="bloquant",
category="missing_info",
message="DP manquant",
affected_codes=[],
suggested_action="Ajouter un DP",
)
questions = question_generator.generate_questions(
sample_proposal, sample_stay, [blocking_issue]
)
# Vérifier qu'une question est générée
assert len(questions) > 0
# Vérifier que la question a une priorité haute (1)
assert any(q.priority == 1 for q in questions)
def test_generate_from_review_issues(
self, question_generator, sample_proposal, sample_stay
):
"""Test la génération de questions depuis des problèmes à revoir."""
review_issue = ValidationIssue(
issue_id="i1",
severity="a_revoir",
category="contradiction",
message="Incohérence détectée",
affected_codes=["K29.1"],
suggested_action="Vérifier le code",
)
questions = question_generator.generate_questions(
sample_proposal, sample_stay, [review_issue]
)
# Vérifier qu'une question est générée
assert len(questions) > 0
# Vérifier que la question a une priorité moyenne (2)
assert any(q.priority == 2 for q in questions)
def test_no_questions_from_info_issues(
self, question_generator, sample_proposal, sample_stay
):
"""Test qu'aucune question n'est générée depuis des problèmes info."""
info_issue = ValidationIssue(
issue_id="i1",
severity="info",
category="other",
message="Information",
affected_codes=[],
suggested_action="Aucune",
)
questions = question_generator.generate_questions(
sample_proposal, sample_stay, [info_issue]
)
# Les questions info ne génèrent pas de questions
# (mais d'autres sources peuvent en générer)
# Donc on vérifie juste que ça ne plante pas
assert isinstance(questions, list)
def test_generate_for_suspected_facts(
self, question_generator, sample_proposal, sample_document
):
"""Test la génération de questions pour faits suspectés."""
suspected_fact = ClinicalFact(
fact_id="fact_001",
type="diagnostic",
text="gastrite",
qualifier=Qualifier(
certainty="suspecté",
markers=["possible"],
confidence=0.7,
),
temporality="actuel",
evidence=Evidence(
document_id="doc_001",
span=Span(start=20, end=35),
text="possible gastrite",
context="Patient présente une possible gastrite",
),
confidence=0.7,
)
stay = StructuredStay(
stay_id="stay_001",
documents=[sample_document],
sections=[],
facts=[suspected_fact],
)
questions = question_generator.generate_questions(
sample_proposal, stay, []
)
# Vérifier qu'une question est générée pour le fait suspecté
suspected_questions = [
q for q in questions
if "suspecté" in q.text.lower() or "confirmé" in q.text.lower()
]
assert len(suspected_questions) > 0
assert suspected_questions[0].category == "clarification"
def test_generate_for_low_confidence_codes(
self, question_generator, sample_stay
):
"""Test la génération de questions pour codes à faible confiance."""
low_confidence_code = Code(
code="K29.1",
label="Gastrite",
type="dp",
evidence=[sample_stay.facts[0].evidence],
confidence=0.5, # Confiance faible
reasoning="Diagnostic incertain",
referentiel_version="2026",
)
proposal = CodingProposal(
stay_id="stay_001",
dp=low_confidence_code,
dr=None,
das=[],
ccam=[],
reasoning="Test",
model_version=ModelVersion(
model_name="test", model_tag="v1", model_digest="a" * 64
),
prompt_version="v1",
)
questions = question_generator.generate_questions(
proposal, sample_stay, []
)
# Vérifier qu'une question est générée pour la faible confiance
confidence_questions = [
q for q in questions
if "confiance" in q.text.lower() or "confirmer" in q.text.lower()
]
assert len(confidence_questions) > 0
class TestInconsistencyDetection:
"""Tests pour la détection d'incohérences."""
def test_detect_negated_fact_with_code(
self, question_generator, sample_document
):
"""Test la détection d'un fait nié avec code proposé."""
negated_fact = ClinicalFact(
fact_id="fact_001",
type="diagnostic",
text="gastrite",
qualifier=Qualifier(
certainty="nié",
markers=["pas de"],
confidence=0.9,
),
temporality="actuel",
evidence=Evidence(
document_id="doc_001",
span=Span(start=20, end=35),
text="pas de gastrite",
context="Patient ne présente pas de gastrite",
),
confidence=0.9,
)
stay = StructuredStay(
stay_id="stay_001",
documents=[sample_document],
sections=[],
facts=[negated_fact],
)
# Code pour le diagnostic nié
code = Code(
code="K29.1",
label="Gastrite",
type="dp",
evidence=[negated_fact.evidence],
confidence=0.8,
reasoning="Test",
referentiel_version="2026",
)
proposal = CodingProposal(
stay_id="stay_001",
dp=code,
dr=None,
das=[],
ccam=[],
reasoning="Test",
model_version=ModelVersion(
model_name="test", model_tag="v1", model_digest="a" * 64
),
prompt_version="v1",
)
questions = question_generator.generate_questions(
proposal, stay, []
)
# Vérifier qu'une question d'incohérence est générée
inconsistency_questions = [
q for q in questions
if q.category == "contradiction" and "nié" in q.text.lower()
]
assert len(inconsistency_questions) > 0
# Vérifier que la priorité est haute (1)
assert inconsistency_questions[0].priority == 1
def test_detect_document_contradictions(
self, question_generator, sample_proposal
):
"""Test la détection de contradictions entre documents."""
# Créer deux faits contradictoires dans différents documents
fact1 = ClinicalFact(
fact_id="fact_001",
type="diagnostic",
text="gastrite",
qualifier=Qualifier(
certainty="affirmé",
markers=[],
confidence=0.9,
),
temporality="actuel",
evidence=Evidence(
document_id="doc_001",
span=Span(start=20, end=35),
text="gastrite confirmée",
context="Patient présente une gastrite confirmée",
),
confidence=0.9,
)
fact2 = ClinicalFact(
fact_id="fact_002",
type="diagnostic",
text="gastrite",
qualifier=Qualifier(
certainty="nié",
markers=["pas de"],
confidence=0.9,
),
temporality="actuel",
evidence=Evidence(
document_id="doc_002",
span=Span(start=10, end=25),
text="pas de gastrite",
context="Examen ne montre pas de gastrite",
),
confidence=0.9,
)
doc1 = ClinicalDocument(
document_id="doc_001",
document_type="cr_medical",
content="Test",
creation_date=datetime(2024, 1, 15),
priority=2,
)
doc2 = ClinicalDocument(
document_id="doc_002",
document_type="imagerie",
content="Test",
creation_date=datetime(2024, 1, 16),
priority=3,
)
stay = StructuredStay(
stay_id="stay_001",
documents=[doc1, doc2],
sections=[],
facts=[fact1, fact2],
)
questions = question_generator.generate_questions(
sample_proposal, stay, []
)
# Vérifier qu'une question de contradiction est générée
contradiction_questions = [
q for q in questions
if q.category == "contradiction" and "contradiction" in q.text.lower()
]
assert len(contradiction_questions) > 0
class TestQuestionPrioritization:
"""Tests pour la priorisation des questions."""
def test_questions_sorted_by_priority(self, question_generator):
"""Test que les questions sont triées par priorité."""
# Créer des questions avec différentes priorités
from pipeline_mco_pmsi.models.validation import Question
questions = [
Question(
question_id="q1",
text="Question priorité 3",
priority=3,
category="confirmation",
context="Test",
suggested_answers=[],
),
Question(
question_id="q2",
text="Question priorité 1",
priority=1,
category="contradiction",
context="Test",
suggested_answers=[],
),
Question(
question_id="q3",
text="Question priorité 2",
priority=2,
category="clarification",
context="Test",
suggested_answers=[],
),
]
# Utiliser la méthode de priorisation
sorted_questions = question_generator._prioritize_and_limit(questions)
# Vérifier que les questions sont triées par priorité
priorities = [q.priority for q in sorted_questions]
assert priorities == sorted(priorities)
assert sorted_questions[0].priority == 1
def test_contradiction_category_prioritized(self, question_generator):
"""Test que les contradictions sont priorisées."""
from pipeline_mco_pmsi.models.validation import Question
questions = [
Question(
question_id="q1",
text="Question confirmation",
priority=2,
category="confirmation",
context="Test",
suggested_answers=[],
),
Question(
question_id="q2",
text="Question contradiction",
priority=2,
category="contradiction",
context="Test",
suggested_answers=[],
),
]
sorted_questions = question_generator._prioritize_and_limit(questions)
# Avec la même priorité, la contradiction doit venir en premier
assert sorted_questions[0].category == "contradiction"
def test_max_questions_limit_enforced(self, question_generator):
"""Test que la limite MAX_QUESTIONS est respectée."""
from pipeline_mco_pmsi.models.validation import Question
# Créer plus de MAX_QUESTIONS questions
many_questions = [
Question(
question_id=f"q{i}",
text=f"Question {i}",
priority=i % 5 + 1,
category="confirmation",
context="Test",
suggested_answers=[],
)
for i in range(10)
]
limited_questions = question_generator._prioritize_and_limit(many_questions)
# Vérifier que seulement MAX_QUESTIONS sont retournées
assert len(limited_questions) == QuestionGenerator.MAX_QUESTIONS
# Vérifier que ce sont les plus prioritaires
assert all(q.priority <= 3 for q in limited_questions)