"""TDD — signature de trajectoire (Phase 0 ; primitive partagée SP-4 / SP-2 / compétences). Propriété centrale : la signature identifie une TRAJECTOIRE (séquence d'actions sur des cibles stables). Elle doit être **stable entre sessions** — donc indépendante des champs session-spécifiques (IDs de nœuds, timestamps, coordonnées). C'est ce qui rend le create-or-update (décision F1) possible : deux apprentissages du même parcours = même id. """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from core.execution.trajectory_signature import trajectory_signature def test_deterministic_same_sequence(): steps = [ {"action_type": "mouse_click", "target": "menu Fichier"}, {"action_type": "text_input", "target": "champ recherche"}, ] assert trajectory_signature(steps) == trajectory_signature(steps) def test_ignores_session_specific_fields(): """Deux sessions du MÊME parcours (mêmes action_type+target) mais IDs de nœuds / timestamps / coords différents → MÊME signature.""" session_a = [ {"action_type": "mouse_click", "target": "menu Fichier", "node_id": "n_abc", "timestamp": 1000, "x": 12, "y": 34}, {"action_type": "text_input", "target": "champ recherche", "node_id": "n_def", "timestamp": 1100, "x": 50, "y": 60}, ] session_b = [ {"action_type": "mouse_click", "target": "menu Fichier", "node_id": "n_zzz", "timestamp": 9000, "x": 99, "y": 88}, {"action_type": "text_input", "target": "champ recherche", "node_id": "n_yyy", "timestamp": 9100, "x": 11, "y": 22}, ] assert trajectory_signature(session_a) == trajectory_signature(session_b) def test_order_sensitive(): a = [{"action_type": "mouse_click", "target": "A"}, {"action_type": "text_input", "target": "B"}] b = list(reversed(a)) assert trajectory_signature(a) != trajectory_signature(b) def test_target_discriminates(): a = [{"action_type": "mouse_click", "target": "bouton Valider"}] b = [{"action_type": "mouse_click", "target": "bouton Annuler"}] assert trajectory_signature(a) != trajectory_signature(b) def test_returns_sha256_hex(): sig = trajectory_signature([{"action_type": "mouse_click", "target": "x"}]) assert len(sig) == 64 assert all(c in "0123456789abcdef" for c in sig) # --------------------------------------------------------------------------- # R1/R2 amendés — verdict Qwen 2026-06-25 : normalisation déterministe + PII # neutralisée par regex DÉDIÉES (pas de pii_blur, pas de NER). Stabilité # labo/DGX = portabilité de la signature. Noms sans titre : stratégie (b) # (impact 0 en labo, gate = audit agrégat DGX avant prod). # --------------------------------------------------------------------------- def test_target_normalized_case_and_accents(): """Q2 : casse et accents ne changent pas la signature (même cible sémantique).""" a = [{"action_type": "mouse_click", "target": "Valider"}] b = [{"action_type": "mouse_click", "target": "VALIDER"}] c = [{"action_type": "mouse_click", "target": "validér"}] assert trajectory_signature(a) == trajectory_signature(b) == trajectory_signature(c) def test_pii_ipp_neutralized(): """R1 : deux IPP différents sur le même champ → MÊME signature (PII neutralisée). Et une cible sans identifiant reste discriminée.""" a = [{"action_type": "mouse_click", "target": "Patient IPP 25012257"}] b = [{"action_type": "mouse_click", "target": "Patient IPP 30045678"}] assert trajectory_signature(a) == trajectory_signature(b) c = [{"action_type": "mouse_click", "target": "Patient liste"}] assert trajectory_signature(a) != trajectory_signature(c) def test_pii_date_neutralized(): """R1 : deux dates différentes → MÊME signature.""" a = [{"action_type": "mouse_click", "target": "RDV du 12/05/2026"}] b = [{"action_type": "mouse_click", "target": "RDV du 03/11/2025"}] assert trajectory_signature(a) == trajectory_signature(b) def test_pii_phone_and_email_neutralized(): """R1 : téléphone (FR) et email neutralisés (deux valeurs distinctes → même sig).""" tel_a = [{"action_type": "text_input", "target": "tel 06 12 34 56 78"}] tel_b = [{"action_type": "text_input", "target": "tel 07 98 76 54 32"}] assert trajectory_signature(tel_a) == trajectory_signature(tel_b) mail_a = [{"action_type": "text_input", "target": "mail jean.dupont@chu.fr"}] mail_b = [{"action_type": "text_input", "target": "mail m.martin@chu.fr"}] assert trajectory_signature(mail_a) == trajectory_signature(mail_b)