"""Tests de la fonction build_dpi_enriched (core/llm/t2a_decision). Voir docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md pour le contexte. Cas pilote : MOREL Catherine (IPP 25003284), passage urgences 01/01/2025 03:12 → 06:49 (3h37 réelles). Bug initial : LLM hallucine 23h (confusion avec "depuis 23h" présent dans Observ. IDE Urg). """ from __future__ import annotations from core.llm.t2a_decision import build_dpi_enriched # ---------------------------------------------------------------------------- # Fixtures DPI — reproduisent le format texte attendu en sortie OCR scroll auto. # Le bandeau Easily Assure est répété en tête de chaque onglet (effet de bord # de extract_text(top_var) qui capture la zone visible incluant l'en-tête fixe). # ---------------------------------------------------------------------------- _BANDEAU_MOREL = ( "IPP : 25003284 MOREL Catherine Né(e) le 14/03/1947 | 77 ans | Sexe : F | " "Arrivée : 01/01/2025 03:12 | IAO : CARON Sandrine (03:25) | " "Médecin : BONNET Antoine | Sortie : 01/01/2025 06:49" ) _BANDEAU_MOREL_SANS_SORTIE = ( "IPP : 25003284 MOREL Catherine Né(e) le 14/03/1947 | 77 ans | Sexe : F | " "Arrivée : 01/01/2025 03:12 | IAO : CARON Sandrine (03:25) | " "Médecin : BONNET Antoine" ) def _build_dpi_morel( bandeau: str = _BANDEAU_MOREL, inclure_date_decision: bool = True, ) -> str: """Construit un DPI MOREL plausible (bandeau répété 5x + sections d'onglets + Synthèse Urgences). Permet de générer la variante "test négatif" en retirant la Date de décision médicale. """ date_decision_ligne = ( "Date de décision médicale 01/01/2025 à 06:49\n" if inclure_date_decision else "" ) return f"""{bandeau} Motif d'admission : Asthme — quintes de toux sèche depuis 23h, fièvre, tachycardie. {bandeau} Examens cliniques : À l'admission : fièvre 39.2°C, tachycardie 91 bpm, peakflow 260. Sibilants bilatéraux, conscience normale. {bandeau} Imagerie : Condensation parenchymateuse basithoracique gauche avec émoussement du cul-de-sac pleural. {bandeau} Notes médicales : Bilan biologique : hyperleucocytose 11 000 GB, CRP 25. PCR positive pour virus respiratoire syncytial. Traitement : Augmentin + Ventoline 3/j. {bandeau} Synthèse Urgences Détails de l'épisode Episode - Date 01/01/2025 à 03:12 Mode de transport à l'arrivée Véhicule personnel Médicalisation du transport Aucune médicalisation Mode d'entrée Autres admissions urgentes Origine du transfert Détails de l'orientation aux Urgences Date d'orientation 01/01/2025 à 03:25 IAO CARON Sandrine Priorité Priorité 3 Episode - Sous-type Médecine Circonstances Motif de prise en charge Asthme Observ. IDE Urg depuis 23h , quintes de toux sèche a pris 2+2 B de VENTO = inefficace ATCD = asthme , insuf coronarienne Détails de la prise en charge Médecin de la prise en charge médicale BONNET Antoine Date de la prise en charge médicale 01/01/2025 à 03:12 CCMU 3. Etat lésionnel et/ou pronostic fonctionnel jugés susceptibles de s'aggraver aux urgences ou durant l'intervention du SMUR, sans mettre en jeu le pronostic vital GEMSA 2. Patient non convoqué sortant après consultation ou soins. Diagnostics J12.1 Pneumopathie due au virus respiratoire syncytial [VRS] [CMA2] - actif Décision médicale Médecin de la décision médicale BONNET Antoine {date_decision_ligne}Décision médicale Consultation externe Orientation du patient US de destination UC CONSULT.URGENCES """ # ---------------------------------------------------------------------------- # Test golden — cas MOREL complet # ---------------------------------------------------------------------------- def test_golden_morel_dpi_enriched_string(): """Le bloc FAITS_CALCULÉS contient toutes les valeurs attendues.""" dpi_raw = _build_dpi_morel() dpi_enriched, _metadata = build_dpi_enriched(dpi_raw) # Durée — format humain + décimal assert "Durée totale du passage : 3 heures et 37 minutes" in dpi_enriched assert "(soit 3.62 heures décimales)" in dpi_enriched # Âge assert "Âge du patient : 77 ans" in dpi_enriched # CCMU libellé COMPLET (début + portion fin = reconstitution multi-ligne) assert "CCMU : 3. Etat lésionnel" in dpi_enriched assert "sans mettre en jeu le pronostic vital" in dpi_enriched # GEMSA libellé COMPLET assert "GEMSA : 2. Patient non convoqué sortant après consultation ou soins" in dpi_enriched # Priorité IAO assert "Priorité IAO : Priorité 3" in dpi_enriched # Diagnostic principal assert "J12.1 Pneumopathie" in dpi_enriched # Bloc FAITS_CALCULÉS positionné AVANT le bandeau brut idx_faits = dpi_enriched.index("FAITS_CALCULÉS") idx_bandeau = dpi_enriched.index("IPP : 25003284") assert idx_faits < idx_bandeau, "FAITS_CALCULÉS doit être en tête du DPI" # decision_terrain et orientation_terrain ABSENTS du bloc FAITS_CALCULÉS # (ils sont dans metadata mais ne doivent pas biaiser le LLM) bloc_faits = dpi_enriched.split("\n\n", 1)[0] assert "Consultation externe" not in bloc_faits assert "UC CONSULT.URGENCES" not in bloc_faits assert "Décision médicale terrain" not in bloc_faits def test_golden_morel_metadata_complet(): """Le dict metadata contient toutes les valeurs Python extraites.""" dpi_raw = _build_dpi_morel() _enriched, metadata = build_dpi_enriched(dpi_raw) # Durée précise (tolérance arrondi) assert metadata["duree_heures_decimales"] is not None assert abs(metadata["duree_heures_decimales"] - 3.62) < 0.01 # Âge assert metadata["age_ans"] == 77 # CCMU/GEMSA libellé complet (validation startswith + extrait fin) assert metadata["ccmu"] is not None assert metadata["ccmu"].startswith("3.") assert "Etat lésionnel" in metadata["ccmu"] assert "pronostic vital" in metadata["ccmu"] assert metadata["gemsa"] is not None assert metadata["gemsa"].startswith("2.") assert "non convoqué" in metadata["gemsa"] # Priorité IAO assert metadata["priorite_iao"] == "Priorité 3" # Mode de venue + médicalisation assert metadata["mode_venue"] == "Véhicule personnel" assert metadata["mode_medicalisation"] == "Aucune médicalisation" assert metadata["mode_entree"] == "Autres admissions urgentes" # Diagnostic principal assert metadata["diagnostic_principal"] is not None assert "J12.1" in metadata["diagnostic_principal"] assert "Pneumopathie" in metadata["diagnostic_principal"] # Décision terrain (metadata uniquement, pour garde-fou serveur commit 2) assert metadata["decision_terrain"] == "Consultation externe" assert metadata["orientation_terrain"] == "UC CONSULT.URGENCES" # Dates parsées assert metadata["date_admission"] is not None assert metadata["date_admission"].strftime("%Y-%m-%d %H:%M") == "2025-01-01 03:12" assert metadata["date_sortie"] is not None assert metadata["date_sortie"].strftime("%Y-%m-%d %H:%M") == "2025-01-01 06:49" # Pas de warning de parsing sur un cas complet assert metadata["parsing_warnings"] == [] # ---------------------------------------------------------------------------- # Test négatif — horaires de sortie absents # ---------------------------------------------------------------------------- def test_negatif_sortie_absente(): """Si Date de décision médicale ET Sortie bandeau sont retirées, la fonction ne crashe pas, signale NON CALCULABLE et accumule un warning. """ dpi_raw = _build_dpi_morel( bandeau=_BANDEAU_MOREL_SANS_SORTIE, inclure_date_decision=False, ) dpi_enriched, metadata = build_dpi_enriched(dpi_raw) # Pas de crash, retour valide assert isinstance(dpi_enriched, str) assert isinstance(metadata, dict) # Ligne explicite de signalement dans FAITS_CALCULÉS assert "Durée totale du passage : NON CALCULABLE" in dpi_enriched # Metadata : duree None, date_sortie None assert metadata["duree_heures_decimales"] is None assert metadata["date_sortie"] is None # Admission reste détectable (Episode-Date + bandeau Arrivée présents) assert metadata["date_admission"] is not None # Warnings explicites assert metadata["parsing_warnings"] assert any("date_sortie" in w for w in metadata["parsing_warnings"]) # ---------------------------------------------------------------------------- # Test robustesse — DPI vide / malformé ne crashe pas # ---------------------------------------------------------------------------- def test_dpi_vide_ne_crashe_pas(): """Un DPI vide retourne un tuple valide avec metadata tous None.""" dpi_enriched, metadata = build_dpi_enriched("") assert isinstance(dpi_enriched, str) assert dpi_enriched.startswith("FAITS_CALCULÉS") assert metadata["duree_heures_decimales"] is None assert metadata["age_ans"] is None assert metadata["ccmu"] is None assert metadata["decision_terrain"] is None # Warnings accumulés sur tous les champs critiques assert metadata["parsing_warnings"]