Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped
Pipeline E2E complet validé : Capture VM → streaming → serveur → cleaner → replay → audit trail Mode apprentissage supervisé fonctionne (Léa échoue → humain → reprise) Dashboard : - Cleanup 14→10 onglets (RCE supprimée) - Fleet : enregistrer/révoquer agents, tokens, ZIP pré-configuré téléchargeable - Audit trail MVP (/audit) : filtres, tableau, export CSV, conformité AI Act/RGPD - Formulaire Fleet simplifié (nom + email, machine_id auto) VWB bridge Léa→VWB : - Compound décomposés en N steps (saisie + raccourci visibles) - Layout serpentin 3 colonnes (plus colonne verticale) - Badge OS 🪟/🐧, filtre OS retiré (admin Linux voit Windows) - Fix import SQLite readonly Cleaner intelligent : - Descriptions lisibles (UIA/C2) + détection doublons - Logique C2 : UIElement identifié = jamais parasite - Patterns parasites resserrés - Message Léa : "Je n'y arrive pas, montrez-moi comment faire" Config agent (INC-1 à INC-7) : - SERVER_URL + SERVER_BASE unifiés - RPA_OLLAMA_HOST séparé - allow_redirects=False sur POST - Middleware réécriture URL serveur CI Gitea : fix token + Flask-SocketIO + ruff propre Fleet endpoints : /agents/enroll|uninstall|fleet + agent_registry SQLite Backup : script quotidien workflows.db + audit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
656 lines
24 KiB
Python
656 lines
24 KiB
Python
"""Tests pour tools/session_cleaner.py — precision de la detection parasite.
|
|
|
|
Ces tests verifient que la detection parasite est assez fine pour les
|
|
logiciels metier hospitaliers (DPI, codage PMSI, facturation) ou les
|
|
interfaces sont complexes (onglets, dialogues, assistants).
|
|
|
|
Regle d'or : mieux vaut garder un parasite que supprimer un vrai clic.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
import pytest
|
|
|
|
# Ajouter le repertoire racine au path pour importer tools.session_cleaner
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from tools.session_cleaner import (
|
|
_ACTIONABLE_TYPES,
|
|
_PARASITIC_WINDOW_PATTERNS,
|
|
_get_app_name,
|
|
_has_identified_ui_element,
|
|
_is_parasitic,
|
|
_is_stop_recording_event,
|
|
_is_systray_interaction,
|
|
_parse_actions,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers pour construire des evenements de test
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_event(
|
|
etype: str = "mouse_click",
|
|
window_title: str = "",
|
|
app_name: str = "",
|
|
button: str = "left",
|
|
pos: list = None,
|
|
keys: list = None,
|
|
text: str = "",
|
|
uia_name: str = "",
|
|
uia_parent_path: list = None,
|
|
ui_elements: list = None,
|
|
vision_ui_elements: list = None,
|
|
) -> Dict[str, Any]:
|
|
"""Construire un evenement minimal pour les tests.
|
|
|
|
Parametres supplementaires pour la logique C2/UIA :
|
|
- uia_name : nom de l'element UIA (bouton, champ, etc.)
|
|
- ui_elements : liste d'elements C2 detectes par le pipeline vision
|
|
- vision_ui_elements : idem, place dans vision_info.ui_elements
|
|
"""
|
|
inner: Dict[str, Any] = {"type": etype}
|
|
|
|
if window_title or app_name:
|
|
inner["window"] = {"title": window_title, "app_name": app_name}
|
|
|
|
if etype == "mouse_click":
|
|
inner["button"] = button
|
|
inner["pos"] = pos or [640, 400]
|
|
|
|
if etype in ("key_combo", "key_press"):
|
|
inner["keys"] = keys or []
|
|
|
|
if etype in ("text_input", "type"):
|
|
inner["text"] = text
|
|
|
|
if uia_name or uia_parent_path:
|
|
inner["uia_snapshot"] = {
|
|
"name": uia_name,
|
|
"control_type": "",
|
|
"parent_path": uia_parent_path or [],
|
|
}
|
|
|
|
if ui_elements is not None:
|
|
inner["ui_elements"] = ui_elements
|
|
|
|
if vision_ui_elements is not None:
|
|
inner["vision_info"] = {"ui_elements": vision_ui_elements}
|
|
|
|
return {"event": inner, "session_id": "test_sess", "machine_id": "test"}
|
|
|
|
|
|
def _wrap_session(actionable_events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""Entourer d'evenements non-exploitables pour simuler une vraie session."""
|
|
hb = {"event": {"type": "heartbeat"}, "session_id": "test_sess", "machine_id": "test"}
|
|
return [hb] + actionable_events + [hb]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bug 1 — Premier clic sur le bureau (Program Manager) pas parasite
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBug1ProgramManager:
|
|
"""Program Manager ne doit plus jamais etre marque parasite."""
|
|
|
|
def test_first_click_on_desktop_not_parasitic(self):
|
|
"""Premier clic avec window='Program Manager' → NOT parasite.
|
|
|
|
Cas reel : l'utilisateur demarre un workflow depuis le bureau
|
|
Windows en cliquant sur une icone ou la barre des taches.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Program Manager",
|
|
app_name="explorer.exe",
|
|
pos=[640, 400],
|
|
)
|
|
assert not _is_parasitic(event, index=0, total=10)
|
|
|
|
def test_middle_click_on_program_manager_not_parasitic(self):
|
|
"""Clic milieu de workflow sur bureau → NOT parasite.
|
|
|
|
Choix pragmatique : meme si l'utilisateur clique dans le vide,
|
|
c'est a lui de decider via l'interface du cleaner. Pas de
|
|
suppression automatique.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Program Manager",
|
|
app_name="explorer.exe",
|
|
pos=[640, 400],
|
|
)
|
|
# Index 5 sur 10 = milieu de session
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_program_manager_not_in_patterns(self):
|
|
"""'program manager' ne doit pas figurer dans les patterns parasites."""
|
|
for pattern in _PARASITIC_WINDOW_PATTERNS:
|
|
assert "program manager" not in pattern.lower(), (
|
|
f"'program manager' ne doit pas etre dans _PARASITIC_WINDOW_PATTERNS "
|
|
f"(trouve dans '{pattern}')"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bug 2 — Derniers clics : pas de regle blanket "3 derniers"
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBug2LastEventsNotBlanketParasitic:
|
|
"""Les derniers clics ne doivent plus etre automatiquement parasites."""
|
|
|
|
def test_last_click_on_notepad_not_parasitic(self):
|
|
"""Dernier clic avec window='Bloc-notes' → NOT parasite.
|
|
|
|
Cas reel : l'utilisateur valide/sauvegarde dans son dernier clic.
|
|
L'ancienne regle supprimait systematiquement les 3 derniers clics.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
pos=[500, 300],
|
|
)
|
|
# Dernier evenement sur 10
|
|
assert not _is_parasitic(event, index=9, total=10)
|
|
|
|
def test_last_click_on_dpi_not_parasitic(self):
|
|
"""Dernier clic sur DPI urgences → NOT parasite.
|
|
|
|
Cas critique : le dernier clic est souvent 'Valider le codage'
|
|
ou 'Sauvegarder la fiche patient'.
|
|
"""
|
|
event = _make_event(
|
|
window_title="DPI Urgences - Validation du codage",
|
|
app_name="dpi.exe",
|
|
pos=[800, 600],
|
|
)
|
|
assert not _is_parasitic(event, index=9, total=10)
|
|
|
|
def test_second_to_last_not_parasitic(self):
|
|
"""Avant-dernier clic metier → NOT parasite."""
|
|
event = _make_event(
|
|
window_title="Facturation MCO - Saisie actes",
|
|
app_name="facturation.exe",
|
|
pos=[500, 400],
|
|
)
|
|
assert not _is_parasitic(event, index=8, total=10)
|
|
|
|
def test_last_click_on_lea_systray_is_parasitic_via_stop(self):
|
|
"""Dernier clic sur Lea RPA (pythonw.exe) → parasite via stop_recording.
|
|
|
|
L'utilisateur clique sur l'icone Lea dans la systray pour arreter.
|
|
"""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="pythonw.exe",
|
|
pos=[917, 522],
|
|
uia_name="Contexte",
|
|
)
|
|
assert _is_stop_recording_event(event, is_last_actionable=True)
|
|
|
|
def test_not_last_click_on_pythonw_not_stop(self):
|
|
"""Clic sur pythonw.exe qui n'est PAS le dernier → pas stop_recording."""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="pythonw.exe",
|
|
pos=[917, 522],
|
|
)
|
|
assert not _is_stop_recording_event(event, is_last_actionable=False)
|
|
|
|
def test_ctrl_shift_l_stop_is_parasitic(self):
|
|
"""key_combo Ctrl+Shift+L en dernier → parasite (arret explicite)."""
|
|
event = _make_event(
|
|
etype="key_combo",
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
keys=["ctrl", "shift", "l"],
|
|
)
|
|
assert _is_stop_recording_event(event, is_last_actionable=True)
|
|
|
|
def test_ctrl_shift_l_not_last_not_stop(self):
|
|
"""Ctrl+Shift+L au milieu de session → pas un arret."""
|
|
event = _make_event(
|
|
etype="key_combo",
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
keys=["ctrl", "shift", "l"],
|
|
)
|
|
assert not _is_stop_recording_event(event, is_last_actionable=False)
|
|
|
|
def test_ctrl_s_last_not_stop(self):
|
|
"""Ctrl+S en dernier → NOT stop_recording (c'est un Ctrl+S, pas Ctrl+Shift).
|
|
|
|
Sauvegarder est un geste metier, pas un arret.
|
|
"""
|
|
event = _make_event(
|
|
etype="key_combo",
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
keys=["ctrl", "s"],
|
|
)
|
|
# Ctrl+S n'a pas 'shift', donc pas un stop
|
|
assert not _is_stop_recording_event(event, is_last_actionable=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bug 3 — Pattern "assistant" trop large
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBug3AssistantPattern:
|
|
"""'assistant' ne doit plus etre un pattern parasite."""
|
|
|
|
def test_assistant_in_dpi_not_parasitic(self):
|
|
"""Clic avec window='Assistant de codage PMSI' → NOT parasite.
|
|
|
|
Les logiciels metier hospitaliers utilisent souvent 'Assistant'
|
|
dans leurs titres de fenetre.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Assistant de codage PMSI",
|
|
app_name="dpi.exe",
|
|
pos=[600, 400],
|
|
)
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_assistant_facturation_not_parasitic(self):
|
|
"""'Assistant facturation' → NOT parasite."""
|
|
event = _make_event(
|
|
window_title="Assistant facturation - Etape 2/4",
|
|
app_name="facturation.exe",
|
|
pos=[500, 300],
|
|
)
|
|
assert not _is_parasitic(event, index=3, total=10)
|
|
|
|
def test_assistant_saisie_not_parasitic(self):
|
|
"""'Assistant de saisie' → NOT parasite."""
|
|
event = _make_event(
|
|
window_title="Assistant de saisie des actes CCAM",
|
|
app_name="saisie.exe",
|
|
pos=[700, 500],
|
|
)
|
|
assert not _is_parasitic(event, index=4, total=10)
|
|
|
|
def test_assistant_not_in_patterns(self):
|
|
"""'assistant' ne doit pas figurer tel quel dans les patterns."""
|
|
for pattern in _PARASITIC_WINDOW_PATTERNS:
|
|
# "assistant" seul ne doit pas y etre ;
|
|
# "lea - rpa assistant" ou similaire est OK car specifique
|
|
if pattern == "assistant":
|
|
pytest.fail(
|
|
"'assistant' seul est trop large pour les logiciels metier. "
|
|
"Utiliser un pattern plus specifique si necessaire."
|
|
)
|
|
|
|
def test_lea_rpa_pattern_matches(self):
|
|
"""'Léa - RPA' est bien detecte comme parasite."""
|
|
event = _make_event(
|
|
window_title="Léa - RPA Vision",
|
|
app_name="pythonw.exe",
|
|
pos=[400, 300],
|
|
)
|
|
assert _is_parasitic(event, index=5, total=10)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Systray detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSystrayDetection:
|
|
"""La detection d'interaction systray doit etre precise."""
|
|
|
|
def test_icones_cachees_is_systray(self):
|
|
"""Clic sur 'Afficher les icônes cachées' → systray."""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="explorer.exe",
|
|
pos=[1080, 773],
|
|
uia_name="Afficher les icônes cachées",
|
|
)
|
|
assert _is_systray_interaction(event)
|
|
|
|
def test_depassement_parent_is_systray(self):
|
|
"""Element dont le parent est 'Fenêtre de dépassement' → systray."""
|
|
event = _make_event(
|
|
window_title="Fenêtre de dépassement",
|
|
app_name="explorer.exe",
|
|
pos=[1026, 710],
|
|
uia_parent_path=[
|
|
{"name": "Bureau 1", "control_type": "volet"},
|
|
{"name": "Fenêtre de dépassement de capacité", "control_type": "volet"},
|
|
],
|
|
)
|
|
assert _is_systray_interaction(event)
|
|
|
|
def test_normal_button_not_systray(self):
|
|
"""Bouton normal dans une application → pas systray."""
|
|
event = _make_event(
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
pos=[500, 300],
|
|
uia_name="Enregistrer",
|
|
uia_parent_path=[
|
|
{"name": "Barre de menu", "control_type": "barre de menu"},
|
|
],
|
|
)
|
|
assert not _is_systray_interaction(event)
|
|
|
|
def test_no_uia_not_systray(self):
|
|
"""Pas de uia_snapshot → pas systray."""
|
|
event = _make_event(
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
pos=[500, 300],
|
|
)
|
|
assert not _is_systray_interaction(event)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# C2/UIA — signaux positifs : element identifie = jamais parasite
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestC2UIAPositiveSignals:
|
|
"""Si C2 ou UIA identifie un element UI reel, le clic est preserve.
|
|
|
|
Principe : un clic sur un bouton/champ/onglet identifie par nom dans
|
|
UIA ou par le pipeline C2 est un acte metier reel, meme si la fenetre
|
|
a un titre bizarre ou inconnu.
|
|
"""
|
|
|
|
def test_click_with_uia_element_never_parasitic(self):
|
|
"""Clic avec uia_snapshot.name='Enregistrer' → NOT parasite.
|
|
|
|
Meme si la fenetre a un titre etrange, un element UI nomme
|
|
est la preuve que c'est un vrai clic metier.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Fenetre bizarre",
|
|
app_name="app_metier.exe",
|
|
pos=[600, 400],
|
|
uia_name="Enregistrer",
|
|
)
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_click_with_uia_element_overrides_unknown_window(self):
|
|
"""Clic avec UIA identifie dans 'unknown_window' → NOT parasite.
|
|
|
|
Cas reel : certaines fenetres ont des titres non resolus par
|
|
l'agent mais l'UIA snapshot identifie quand meme l'element.
|
|
"""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="dpi.exe",
|
|
pos=[400, 300],
|
|
uia_name="Valider le codage",
|
|
)
|
|
assert not _is_parasitic(event, index=8, total=10)
|
|
|
|
def test_click_with_c2_ui_elements_never_parasitic(self):
|
|
"""Clic avec ui_elements (pipeline C2) → NOT parasite.
|
|
|
|
Quand le pipeline C2 enrichit l'evenement avec des elements
|
|
visuels detectes, c'est un signal fort de clic metier.
|
|
"""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="app_metier.exe",
|
|
pos=[600, 400],
|
|
ui_elements=[{"label": "Sauvegarder", "bbox": [580, 380, 620, 420]}],
|
|
)
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_click_with_vision_info_ui_elements_never_parasitic(self):
|
|
"""Clic avec vision_info.ui_elements (format alternatif C2) → NOT parasite."""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="facturation.exe",
|
|
pos=[500, 350],
|
|
vision_ui_elements=[{"type": "button", "text": "Confirmer"}],
|
|
)
|
|
assert not _is_parasitic(event, index=6, total=10)
|
|
|
|
def test_uia_systray_still_parasitic(self):
|
|
"""Clic avec UIA identifie MAIS sur la systray → reste parasite.
|
|
|
|
Le shield C2/UIA ne s'applique pas a la systray. Un clic sur
|
|
'Afficher les icônes cachées' est toujours parasite meme si
|
|
l'UIA a identifie l'element.
|
|
"""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="explorer.exe",
|
|
pos=[1080, 773],
|
|
uia_name="Afficher les icônes cachées",
|
|
)
|
|
# _is_parasitic retourne False (UIA identifie + pas pattern fenetre)
|
|
# mais _is_systray_interaction le rattrape
|
|
assert _is_systray_interaction(event)
|
|
|
|
def test_right_click_with_uia_still_parasitic(self):
|
|
"""Clic droit avec UIA identifie → reste parasite.
|
|
|
|
Les clics droit sont un signal dur — meme si l'UIA a identifie
|
|
un element, un clic droit n'est jamais un clic metier exploitable.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Bloc-notes",
|
|
app_name="Notepad.exe",
|
|
pos=[500, 300],
|
|
button="right",
|
|
uia_name="Zone de texte",
|
|
)
|
|
assert _is_parasitic(event, index=5, total=10)
|
|
|
|
|
|
class TestC2AppNameShield:
|
|
"""Si l'app est une application metier connue, les clics sont preserves."""
|
|
|
|
def test_click_in_known_app_not_parasitic(self):
|
|
"""Clic dans app_name='Notepad.exe' sans UIA → NOT parasite.
|
|
|
|
Une vraie application (pas explorer, pas pythonw) est une app
|
|
metier — ses clics sont legitimes meme sans info UIA/C2.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Sans titre",
|
|
app_name="Notepad.exe",
|
|
pos=[400, 300],
|
|
)
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_click_in_dpi_not_parasitic(self):
|
|
"""Clic dans dpi.exe sans UIA → NOT parasite."""
|
|
event = _make_event(
|
|
window_title="DPI Urgences",
|
|
app_name="dpi.exe",
|
|
pos=[600, 400],
|
|
)
|
|
assert not _is_parasitic(event, index=3, total=10)
|
|
|
|
def test_click_in_searchhost_not_parasitic(self):
|
|
"""Clic dans SearchHost.exe → NOT parasite.
|
|
|
|
La recherche Windows (SearchHost.exe) est une application
|
|
normale, pas un process systeme a filtrer.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Rechercher",
|
|
app_name="SearchHost.exe",
|
|
pos=[500, 300],
|
|
)
|
|
assert not _is_parasitic(event, index=1, total=10)
|
|
|
|
def test_click_in_explorer_no_uia_can_be_parasitic(self):
|
|
"""Clic dans explorer.exe sans UIA → peut etre parasite si pattern.
|
|
|
|
explorer.exe est un cas particulier : c'est a la fois le bureau
|
|
(Program Manager) et l'explorateur de fichiers. Sans UIA pour
|
|
distinguer, on laisse les patterns de fenetre decider.
|
|
"""
|
|
event = _make_event(
|
|
window_title="Fenêtre de dépassement de capacité",
|
|
app_name="explorer.exe",
|
|
pos=[1026, 710],
|
|
)
|
|
# explorer.exe est dans _NON_BUSINESS_APPS, donc pas de shield
|
|
# et le pattern "fenêtre de dépassement" matche → parasite
|
|
assert _is_parasitic(event, index=5, total=10)
|
|
|
|
def test_click_in_pythonw_no_shield(self):
|
|
"""Clic dans pythonw.exe → pas de shield (c'est l'agent Lea)."""
|
|
event = _make_event(
|
|
window_title="unknown_window",
|
|
app_name="pythonw.exe",
|
|
pos=[917, 522],
|
|
)
|
|
# pythonw.exe est dans _NON_BUSINESS_APPS, pas de shield app
|
|
# mais pas de pattern de fenetre non plus → pas parasite via _is_parasitic
|
|
# (sera attrape par _is_stop_recording_event si c'est le dernier)
|
|
assert not _is_parasitic(event, index=5, total=10)
|
|
|
|
|
|
class TestHasIdentifiedUIElement:
|
|
"""Tests unitaires pour _has_identified_ui_element()."""
|
|
|
|
def test_uia_name_detected(self):
|
|
"""uia_snapshot avec nom → element identifie."""
|
|
event = _make_event(uia_name="Bouton OK")
|
|
assert _has_identified_ui_element(event)
|
|
|
|
def test_uia_empty_name_not_detected(self):
|
|
"""uia_snapshot avec nom vide → pas d'element identifie."""
|
|
event = _make_event(uia_name="")
|
|
assert not _has_identified_ui_element(event)
|
|
|
|
def test_no_uia_not_detected(self):
|
|
"""Pas de uia_snapshot → pas d'element identifie."""
|
|
event = _make_event()
|
|
assert not _has_identified_ui_element(event)
|
|
|
|
def test_c2_ui_elements_detected(self):
|
|
"""ui_elements (C2) → element identifie."""
|
|
event = _make_event(ui_elements=[{"label": "Sauvegarder"}])
|
|
assert _has_identified_ui_element(event)
|
|
|
|
def test_vision_ui_elements_detected(self):
|
|
"""vision_info.ui_elements → element identifie."""
|
|
event = _make_event(vision_ui_elements=[{"type": "button"}])
|
|
assert _has_identified_ui_element(event)
|
|
|
|
def test_empty_ui_elements_not_detected(self):
|
|
"""ui_elements vide → pas d'element identifie."""
|
|
event = _make_event(ui_elements=[])
|
|
assert not _has_identified_ui_element(event)
|
|
|
|
|
|
class TestGetAppName:
|
|
"""Tests unitaires pour _get_app_name()."""
|
|
|
|
def test_app_name_from_window(self):
|
|
event = _make_event(app_name="Notepad.exe")
|
|
assert _get_app_name(event) == "Notepad.exe"
|
|
|
|
def test_no_app_name(self):
|
|
event = _make_event()
|
|
assert _get_app_name(event) == ""
|
|
|
|
def test_app_name_none_returns_empty(self):
|
|
"""app_name None → chaine vide."""
|
|
event = {"event": {"type": "mouse_click", "window": {"title": "Test", "app_name": None}}}
|
|
assert _get_app_name(event) == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test sur la vraie session de Dom
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRealSessionDom:
|
|
"""Verification sur la session E2E reelle de Dom.
|
|
|
|
Session: sess_20260417T133324_30c2d0
|
|
Workflow: clic Rechercher → texte → clic resultat → Bloc-notes → texte →
|
|
Ctrl+S → systray stop.
|
|
"""
|
|
|
|
SESSION_PATH = os.path.join(
|
|
os.path.dirname(__file__),
|
|
"..", "..",
|
|
"data", "training", "live_sessions", "windows_vm",
|
|
"sess_20260417T133324_30c2d0", "live_events.jsonl",
|
|
)
|
|
|
|
@pytest.fixture
|
|
def real_events(self):
|
|
"""Charger les evenements reels si disponibles."""
|
|
path = Path(self.SESSION_PATH).resolve()
|
|
if not path.is_file():
|
|
pytest.skip(f"Session reelle non disponible : {path}")
|
|
events = []
|
|
with open(path, encoding="utf-8") as f:
|
|
for line in f:
|
|
if line.strip():
|
|
events.append(json.loads(line))
|
|
return events
|
|
|
|
def test_real_session_no_false_positives(self, real_events):
|
|
"""Aucun clic metier ne doit etre marque parasite.
|
|
|
|
Les 7 premiers evenements exploitables sont du workflow reel :
|
|
- Clic Rechercher (taskbar)
|
|
- Texte dans la recherche
|
|
- Clic sur un resultat
|
|
- Clic Agrandir (Bloc-notes)
|
|
- Clic Nouvel onglet
|
|
- Texte 'bijour'
|
|
- Ctrl+S (sauvegarder)
|
|
|
|
Les 4 derniers sont l'arret (systray):
|
|
- Right-click systray
|
|
- Left-click icones cachees
|
|
- Right-click fenetre depassement
|
|
- Left-click menu Lea (pythonw.exe)
|
|
"""
|
|
# Utiliser _parse_actions avec un dossier bidon (pas de shots)
|
|
session_dir = Path(self.SESSION_PATH).parent
|
|
actions = _parse_actions(real_events, session_dir)
|
|
|
|
# 11 evenements exploitables au total
|
|
assert len(actions) == 11, f"Attendu 11 actions, obtenu {len(actions)}"
|
|
|
|
# Les 7 premiers doivent etre OK (pas parasites)
|
|
for idx, action in enumerate(actions[:7]):
|
|
assert not action["is_parasitic"], (
|
|
f"Action {idx} (evt {action['global_index']}) faussement marquee parasite : "
|
|
f"type={action['type']}, win={action['window_title']}"
|
|
)
|
|
|
|
# Les 4 derniers doivent etre parasites (arret enregistrement)
|
|
for idx, action in enumerate(actions[7:], start=7):
|
|
assert action["is_parasitic"], (
|
|
f"Action {idx} (evt {action['global_index']}) devrait etre parasite : "
|
|
f"type={action['type']}, win={action['window_title']}"
|
|
)
|
|
|
|
def test_real_session_parasitic_count(self, real_events):
|
|
"""4 parasites sur 11 — pas plus, pas moins."""
|
|
session_dir = Path(self.SESSION_PATH).parent
|
|
actions = _parse_actions(real_events, session_dir)
|
|
parasitic = [a for a in actions if a["is_parasitic"]]
|
|
ok = [a for a in actions if not a["is_parasitic"]]
|
|
assert len(parasitic) == 4, (
|
|
f"Attendu 4 parasites, obtenu {len(parasitic)} : "
|
|
+ ", ".join(f"evt{a['global_index']}({a['window_title'][:30]})" for a in parasitic)
|
|
)
|
|
assert len(ok) == 7, (
|
|
f"Attendu 7 OK, obtenu {len(ok)}"
|
|
)
|