sanitize_event(event, mapping) applique le principe « Léa apprend l'interface, pas la donnée » (décision Dom 28/06) avant persistance : - text_input -> contenu (text + raw_keys) remplacé par [SAISIE] (option b) : résout la fuite la plus grave (contenu médical) SANS NER ni détection ; - titres de fenêtre (active_window_title + window/to/from.title) : identité patient tokenisée (anonymize_text), app/écran gardés ; cohérence par mapping. Copie défensive (ne mute pas l'event d'origine). 4 tests (9 au total) verts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.3 KiB
Python
139 lines
5.3 KiB
Python
"""Tests de l'assainissement PII des données capturées (titres, texte, OCR).
|
|
|
|
Couche 1 (sans modèle) : filet regex sur la PII structurée (IPP, NIR, TEL,
|
|
EMAIL, AGE) + règles structurelles cliniques (NOM (NAISSANCE) Prénom ;
|
|
[Nom Prénom] des fenêtres PACS), avec tokens TYPÉS et COHÉRENTS ([IPP_1]…).
|
|
|
|
Réutilise l'approche du projet `anonymisation` (placeholders + regex). La
|
|
couche NER (noms libres) viendra en complément. Cas réels remontés en clinique
|
|
le 28/06 (anonymisés ici par construction). Branche feat/push-log-dgx.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_ROOT = str(Path(__file__).resolve().parents[2])
|
|
if _ROOT not in sys.path:
|
|
sys.path.insert(0, _ROOT)
|
|
|
|
|
|
def test_ipp_et_age_tokenises():
|
|
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
|
|
|
titre = "VIOLA (VIOLA) Liliane 90 ans - IPP: 168246 - Expert Sante - Mozilla Firefox"
|
|
out, ents = anonymize_text(titre)
|
|
|
|
assert "168246" not in out, out # IPP retiré
|
|
assert "[IPP_1]" in out
|
|
assert "90 ans" not in out # âge retiré
|
|
assert "[AGE_1]" in out
|
|
# le nom format clinique « NOM (NAISSANCE) Prénom » est tokenisé
|
|
assert "VIOLA" not in out and "Liliane" not in out, out
|
|
assert "[NOM_1]" in out
|
|
# le logiciel n'est pas pris pour de la PII
|
|
assert "Firefox" in out and "Expert Sante" in out
|
|
types = {e["type"] for e in ents}
|
|
assert {"IPP", "AGE", "NOM"} <= types
|
|
|
|
|
|
def test_nom_entre_crochets_pacs():
|
|
"""Le PACS met le patient entre crochets : `[DATTIN Alix]`."""
|
|
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
|
|
|
titre = "GXD5 Pacs 4.0.4.307 CIM ARES - [DATTIN Alix] - Mozilla Firefox"
|
|
out, _ = anonymize_text(titre)
|
|
|
|
assert "DATTIN" not in out and "Alix" not in out, out
|
|
assert "[NOM_1]" in out
|
|
assert "Pacs" in out and "Firefox" in out # contexte logiciel préservé
|
|
|
|
|
|
def test_coherence_meme_ipp_meme_token():
|
|
"""Même valeur PII -> même token (sur un mapping partagé de session)."""
|
|
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
|
|
|
mapping: dict = {}
|
|
o1, _ = anonymize_text("IPP: 168246 ouvert", mapping=mapping)
|
|
o2, _ = anonymize_text("dossier IPP: 168246 fermé", mapping=mapping)
|
|
o3, _ = anonymize_text("IPP: 270020 autre", mapping=mapping)
|
|
|
|
assert "[IPP_1]" in o1 and "[IPP_1]" in o2 # même patient -> même token
|
|
assert "[IPP_2]" in o3 # patient différent -> token différent
|
|
assert "270020" not in o3
|
|
|
|
|
|
def test_email_et_telephone():
|
|
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
|
|
|
out, _ = anonymize_text("contact j.dupont@chu.fr / 06 12 34 56 78")
|
|
assert "@chu.fr" not in out and "[EMAIL_1]" in out
|
|
assert "06 12 34 56 78" not in out and "[TEL_1]" in out
|
|
|
|
|
|
def test_texte_sans_pii_inchange():
|
|
from agent_v0.server_v1.pii_sanitizer import anonymize_text
|
|
|
|
t = "Expert Sante - Consultation - Mozilla Firefox"
|
|
out, ents = anonymize_text(t)
|
|
assert out == t
|
|
assert ents == []
|
|
|
|
|
|
# --- sanitize_event : assainissement au niveau event (option b pour text_input) ---
|
|
|
|
def test_sanitize_text_input_remplace_contenu_par_saisie():
|
|
"""Option b (Dom) : le contenu tapé n'est pas gardé -> [SAISIE]."""
|
|
from agent_v0.server_v1.pii_sanitizer import sanitize_event
|
|
|
|
ev = {
|
|
"type": "text_input",
|
|
"text": "hemorragie post-operatoire saignement", # contenu médical
|
|
"raw_keys": ["h", "e", "m"],
|
|
"window": {"title": "VIOLA (VIOLA) Liliane 90 ans - IPP: 168246 - Firefox",
|
|
"app_name": "firefox.exe"},
|
|
}
|
|
out = sanitize_event(ev)
|
|
|
|
assert out["text"] == "[SAISIE]"
|
|
assert out["raw_keys"] == "[SAISIE]"
|
|
# le titre de la fenêtre est assaini (identité tokenisée, app gardée)
|
|
assert "168246" not in out["window"]["title"]
|
|
assert "VIOLA" not in out["window"]["title"]
|
|
assert "[IPP_1]" in out["window"]["title"] and "Firefox" in out["window"]["title"]
|
|
# l'event d'origine n'est PAS muté
|
|
assert ev["text"].startswith("hemorragie")
|
|
|
|
|
|
def test_sanitize_heartbeat_titre_direct():
|
|
from agent_v0.server_v1.pii_sanitizer import sanitize_event
|
|
|
|
ev = {"type": "heartbeat",
|
|
"active_window_title": "GXD5 Pacs CIM ARES - [DATTIN Alix] - Firefox"}
|
|
out = sanitize_event(ev)
|
|
assert "DATTIN" not in out["active_window_title"]
|
|
assert "[NOM_1]" in out["active_window_title"] and "Pacs" in out["active_window_title"]
|
|
|
|
|
|
def test_sanitize_focus_change_to_from_window():
|
|
from agent_v0.server_v1.pii_sanitizer import sanitize_event
|
|
|
|
ev = {"type": "window_focus_change",
|
|
"from": None,
|
|
"to": {"title": "LAVAL (BARTHELEMY) Nicole 86 ans - Expert Sante", "app_name": "firefox.exe"},
|
|
"window": {"title": "LAVAL (BARTHELEMY) Nicole 86 ans - Expert Sante"}}
|
|
out = sanitize_event(ev)
|
|
assert out["from"] is None # null géré
|
|
assert "LAVAL" not in out["to"]["title"]
|
|
assert "[NOM_1]" in out["to"]["title"]
|
|
# cohérence : même patient dans to et window -> même token
|
|
assert out["window"]["title"] == out["to"]["title"]
|
|
|
|
|
|
def test_sanitize_action_result_inchange():
|
|
from agent_v0.server_v1.pii_sanitizer import sanitize_event
|
|
|
|
ev = {"type": "action_result", "base_shot_id": "shot_0003", "image": "x.png"}
|
|
assert sanitize_event(ev) == ev
|