Files
rpa_vision_v3/tests/unit/test_stream_event_pii_wiring.py
Dom 6461f0a21b feat(server): câble sanitize_event au chokepoint stream_event (PII)
Assainissement PII appliqué une seule fois à l'entrée de stream_event(),
avec un mapping de tokens par session (cohérence intra-session). Les chemins
de persistance et de traitement (jsonl, worker.process_event_direct,
shadow_observe_event, enrichissement SOM) consomment tous la copie assainie
au lieu de l'event brut — plus aucune PII patient en clair côté serveur.

Test de non-régression du câblage: stream_event ne doit jamais écrire de PII
brute (IPP/contenu saisi) dans live_events.jsonl ni la propager au worker/shadow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:39:27 +02:00

69 lines
2.7 KiB
Python

"""Non-régression sécurité : câblage PII au chokepoint ``stream_event``.
Invariant : un event contenant de la PII patient (titre de fenêtre + contenu
saisi) passé à ``stream_event`` ne doit JAMAIS écrire la PII brute dans le
journal ``live_events.jsonl``, ni la propager au worker ou au shadow observer.
L'assainissement a lieu une seule fois, en amont des chemins de
persistance/traitement (``api_stream.py``, hook ``sanitize_event``).
"""
import asyncio
import json
import os
# Le module serveur refuse de se charger sans token (sécurité prod) ;
# en test unitaire on désactive l'auth pour pouvoir importer le module.
os.environ.setdefault("RPA_AUTH_DISABLED", "true")
import agent_v0.server_v1.api_stream as api
def _event_avec_pii():
# PII captée par la couche 1 : IPP (structurel) + contenu saisi.
# Contexte = logiciel métier réel du POC (pas la maquette Easily abandonnée).
# (Les noms libres sans marqueur relèvent de la couche 2 NER — hors scope ici.)
return {
"type": "text_input",
"text": "anticoagulant 75mg matin",
"active_window_title": "Gxd5diag - Recherche dossier (IPP: 123456)",
}
def test_stream_event_assainit_et_propage_sur_les_chemins(tmp_path, monkeypatch):
"""Le chokepoint applique sanitize_event UNE fois et tous les chemins
(jsonl, worker, shadow) reçoivent la copie assainie — pas la valeur brute."""
captured = {}
monkeypatch.setattr(api, "_ensure_session_registered", lambda *a, **k: None)
monkeypatch.setattr(
api.worker,
"process_event_direct",
lambda sid, ev: (captured.__setitem__("worker", ev), {})[1],
)
monkeypatch.setattr(
api, "shadow_observe_event", lambda sid, ev: captured.__setitem__("shadow", ev)
)
monkeypatch.setattr(api, "LIVE_SESSIONS_DIR", tmp_path)
api._session_pii_mapping.pop("sess_pii", None)
se = api.StreamEvent(
session_id="sess_pii",
machine_id="lea-test",
timestamp=1000.0,
event=_event_avec_pii(),
)
asyncio.run(api.stream_event(se))
# 1. le journal sur disque ne contient ni l'IPP brut ni le contenu saisi
jsonl = (tmp_path / "lea-test" / "sess_pii" / "live_events.jsonl").read_text(
encoding="utf-8"
)
assert "123456" not in jsonl
assert "anticoagulant 75mg" not in jsonl
# 2. contenu saisi masqué + IPP tokenisé (preuve que le titre est traité)
assert "[SAISIE]" in jsonl
assert "[IPP_1]" in jsonl
# 3. worker et shadow reçoivent l'event assaini, pas la valeur brute
assert captured["worker"]["text"] == "[SAISIE]"
assert "123456" not in json.dumps(captured["worker"], ensure_ascii=False)
assert "123456" not in json.dumps(captured["shadow"], ensure_ascii=False)