"""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)