Files
rpa_vision_v3/tests/unit/test_target_resolver_sniper_ranking.py
Dom cf495dd82f feat: chat unifié, GestureCatalog, Copilot, Léa UI, extraction données, vérification replay
Refonte majeure du système Agent Chat et ajout de nombreux modules :

- Chat unifié : suppression du dual Workflows/Agent Libre, tout passe par /api/chat
  avec résolution en 3 niveaux (workflow → geste → "montre-moi")
- GestureCatalog : 38 raccourcis clavier universels Windows avec matching sémantique,
  substitution automatique dans les replays, et endpoint /api/gestures
- Mode Copilot : exécution pas-à-pas des workflows avec validation humaine via WebSocket
  (approve/skip/abort) avant chaque action
- Léa UI (agent_v0/lea_ui/) : interface PyQt5 pour Windows avec overlay transparent
  pour feedback visuel pendant le replay
- Data Extraction (core/extraction/) : moteur d'extraction visuelle de données
  (OCR + VLM → SQLite), avec schémas YAML et export CSV/Excel
- ReplayVerifier (agent_v0/server_v1/) : vérification post-action par comparaison
  de screenshots, avec logique de retry (max 3)
- IntentParser durci : meilleur fallback regex, type GREETING, patterns améliorés
- Dashboard : nouvelles pages gestures, streaming, extractions
- Tests : 63 tests GestureCatalog, 47 tests extraction, corrections tests existants
- Dépréciation : /api/agent/plan et /api/agent/execute retournent HTTP 410,
  suppression du code hardcodé _plan_to_replay_actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 10:02:09 +01:00

127 lines
4.8 KiB
Python

"""
Tests pour Fiche #6 - Sniper Mode : Ranking/Scoring
Auteur: Dom, Alice Kiro - 15 décembre 2024
Objectif: Valider que le resolver classe et choisit le bon élément de manière stable
Tests:
1. Sniper choisit l'élément le plus proche de l'ancre
2. Tie-break stable par element_id
"""
import pytest
# Marquer tous les tests de ce fichier comme fiche6
pytestmark = pytest.mark.fiche6
from datetime import datetime
from core.execution.target_resolver import TargetResolver, ResolutionContext
from core.models.workflow_graph import TargetSpec
from core.models.screen_state import ScreenState, RawLevel, PerceptionLevel, ContextLevel, WindowContext, EmbeddingRef
from core.models.ui_element import UIElement, UIElementEmbeddings, VisualFeatures
def _elem(eid, role, bbox, label="", conf=0.9, etype="ui"):
"""Helper pour créer un UIElement rapidement"""
return UIElement(
element_id=eid,
type=etype,
role=role,
bbox=bbox, # XYWH
center=(bbox[0] + bbox[2] // 2, bbox[1] + bbox[3] // 2),
label=label,
label_confidence=1.0,
embeddings=UIElementEmbeddings(image=None, text=None),
visual_features=VisualFeatures(dominant_color="n/a", has_icon=False, shape="rectangle", size_category="medium"),
confidence=conf,
tags=[],
metadata={},
)
def _screen(elements):
"""Helper pour créer un ScreenState rapidement"""
return ScreenState(
screen_state_id="s1",
timestamp=datetime.now(),
session_id="sess",
window=WindowContext(app_name="app", window_title="win", screen_resolution=[1920, 1080]),
raw=RawLevel(screenshot_path="x.png", capture_method="test", file_size_bytes=1),
perception=PerceptionLevel(
embedding=EmbeddingRef(provider="p", vector_id="v", dimensions=1),
detected_text=[],
text_detection_method="none",
confidence_avg=0.0,
),
context=ContextLevel(),
ui_elements=elements
)
def test_sniper_picks_nearest_to_anchor():
"""Test que le sniper choisit l'élément le plus proche de l'ancre"""
# label + 2 inputs (tous deux "valides" role=input), le plus proche doit gagner
anchor = _elem("lbl_user", "label", (100, 100, 120, 20), "Username", conf=1.0)
near_input = _elem("in_near", "input", (240, 95, 200, 30), "", conf=0.9, etype="text_input")
far_input = _elem("in_far", "input", (240, 300, 200, 30), "", conf=0.9, etype="text_input")
screen = _screen([far_input, anchor, near_input])
spec = TargetSpec(
by_role="input",
context_hints={"near_text": "Username"},
selection_policy="first"
)
r = TargetResolver()
ctx = ResolutionContext(screen_state=screen, previous_target=None)
res = r.resolve_target(spec, screen, ctx)
assert res is not None
assert res.element.element_id == "in_near"
def test_sniper_tie_break_is_stable():
"""Test que le tie-break est stable par element_id"""
# Deux candidats identiques → tie-break par element_id (stable)
anchor = _elem("lbl", "label", (100, 100, 120, 20), "Username", conf=1.0)
a = _elem("a_elem", "input", (240, 95, 200, 30), "", conf=0.9, etype="text_input")
b = _elem("b_elem", "input", (240, 95, 200, 30), "", conf=0.9, etype="text_input")
screen = _screen([anchor, b, a])
spec = TargetSpec(by_role="input", context_hints={"near_text": "Username"})
r = TargetResolver()
ctx = ResolutionContext(screen_state=screen, previous_target=None)
res = r.resolve_target(spec, screen, ctx)
assert res is not None
# Tie-break par element_id : le résultat doit être stable (toujours le même)
# L'ordre dépend du tri interne du resolver (min ou max par element_id)
assert res.element.element_id in ("a_elem", "b_elem")
def test_sniper_debug_info_available():
"""Test que les infos de debug (top3) sont disponibles"""
anchor = _elem("lbl", "label", (100, 100, 120, 20), "Username", conf=1.0)
input1 = _elem("input1", "input", (240, 95, 200, 30), "", conf=0.9, etype="text_input")
input2 = _elem("input2", "input", (240, 150, 200, 30), "", conf=0.8, etype="text_input")
input3 = _elem("input3", "input", (240, 200, 200, 30), "", conf=0.7, etype="text_input")
screen = _screen([anchor, input1, input2, input3])
spec = TargetSpec(by_role="input", context_hints={"near_text": "Username"})
r = TargetResolver()
ctx = ResolutionContext(screen_state=screen, previous_target=None)
res = r.resolve_target(spec, screen, ctx)
assert res is not None
assert hasattr(res, 'resolution_details')
assert 'top3' in res.resolution_details
assert len(res.resolution_details['top3']) <= 3
assert 'anchor_id' in res.resolution_details
assert res.resolution_details['anchor_id'] == "lbl"
if __name__ == "__main__":
pytest.main([__file__, "-v"])