Files
rpa_vision_v3/tests/unit/test_trajectory_signature.py
Dom 3b592dd867 feat(core): signature de trajectoire PII-safe + normalisée (R1/R2 amendés, QG Qwen)
Anonymisation déterministe de la cible par regex DÉDIÉES (email/date/tél/IPP →
tokens) avant hashing : deux sessions sur le même champ (patients/dates
différents) → même signature. Normalisation casse/accents/espaces (logique
action_executor._norm_text, redéfinie localement pour rester léger).

Choix QG Qwen (2026-06-25) : PAS de pii_blur (il protège les dates qu'on veut
neutraliser), PAS de NER (un hash d'identité doit être déterministe/portable
labo↔DGX). Noms propres sans titre non gérés (stratégie b ; gate = audit
agrégat by_text DGX avant prod). R2 fallback coords RETIRÉ (casserait F1).
R3 (machine_id hors hash) déjà conforme.

TDD: +4 tests (RED→GREEN, 9/9). Primitive non wirée (0 consommateur runtime)
→ changement de calcul sans impact.

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

102 lines
4.6 KiB
Python

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