Files
rpa_vision_v3/tests/unit/test_bbox_center_xywh.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

172 lines
6.2 KiB
Python

"""
Tests de validation - Fiche #2 : Validation des calculs de centre BBOX (format XYWH)
Vérifie que tous les calculs de centre utilisent les bons calculs pour le format XYWH :
- BBOX format: (x, y, w, h)
- Centre correct = (x + w/2, y + h/2)
Auteur: Dom, Alice Kiro - 15 décembre 2024
"""
import pytest
from unittest.mock import Mock, patch
from dataclasses import dataclass
from typing import Tuple, List, Optional
from core.execution.action_executor import ActionExecutor
from core.execution.target_resolver import TargetResolver
from core.models.screen_state import ScreenState
from core.models.ui_element import UIElement
from core.models.workflow_graph import Action, ActionType, TargetSpec
@dataclass
class MockUIElement:
"""Mock UIElement pour les tests"""
element_id: str
bbox: Tuple[int, int, int, int] # (x, y, w, h)
label: str = ""
role: str = ""
confidence: float = 0.9
class TestBBoxCenterCalculations:
"""Tests pour vérifier que le format BBOX est cohérent dans tout le système"""
def test_bbox_format_consistency(self):
"""Test que le format BBOX utilise le bon calcul de centre"""
# Définir un BBOX de test
test_bbox = (50, 75, 100, 150) # x=50, y=75, w=100, h=150
# Centre attendu
expected_center = (100, 150) # (50 + 100/2, 75 + 150/2)
# Vérifier que nos calculs sont cohérents
center_x = test_bbox[0] + test_bbox[2] / 2 # x + w/2
center_y = test_bbox[1] + test_bbox[3] / 2 # y + h/2
expected_area = test_bbox[2] * test_bbox[3] # w * h = 100 * 150 = 15000
area = test_bbox[2] * test_bbox[3]
assert (center_x, center_y) == expected_center
assert area == expected_area
# Vérifier que l'ancien calcul (incorrect) donne des résultats différents
old_incorrect_center_x = (test_bbox[0] + test_bbox[2]) / 2 # (x+w)/2 ❌
old_incorrect_center_y = (test_bbox[1] + test_bbox[3]) / 2 # (y+h)/2 ❌
# L'ancien calcul ('incorrect) donnerait un résultat différent
assert center_x != old_incorrect_center_x # 100 != 75 ❌
assert center_y != old_incorrect_center_y # 150 != 112.5 ❌
def test_action_executor_click_position():
"""Test que ActionExecutor calcule correctement la position de clic"""
# Créer une action de clic
action = Mock()
action.type = ActionType.MOUSE_CLICK
action.target = Mock()
action.parameters = {}
action.params = {}
# Mock screen state
screen_state = Mock()
# Créer un élément avec BBOX XYWH
mock_element = MockUIElement(
element_id="test_button",
bbox=(100, 200, 50, 30), # x=100, y=200, w=50, h=30
label="Test Button",
role="button"
)
# Mock du resolved target
mock_resolved = Mock()
mock_resolved.element = mock_element
# Mock du target resolver pour retourner notre élément
with patch('core.execution.action_executor.TargetResolver') as mock_resolver_class:
mock_resolver = Mock()
mock_resolver.resolve_target.return_value = mock_resolved
mock_resolver_class.return_value = mock_resolver
# Mock pyautogui pour capturer les coordonnées de clic
with patch('core.execution.action_executor.pyautogui') as mock_pyautogui:
# Exécuter l'action
executor = ActionExecutor()
result = executor._execute_click(action, screen_state)
# Vérifier que pyautogui.click a été appelé avec les bonnes coordonnées
mock_pyautogui.click.assert_called_once()
call_args = mock_pyautogui.click.call_args[0]
click_x, click_y = call_args
# Centre attendu: (125, 215)
expected_x = 100 + 50 / 2 # 125
expected_y = 200 + 30 / 2 # 215
assert click_x == expected_x
assert click_y == expected_y
def test_target_resolver_position_matching():
"""Test que TargetResolver utilise les bons calculs de centre pour la recherche de position"""
# Créer des éléments de test
elements = [
MockUIElement("elem1", (100, 100, 50, 50)), # centre: (125, 125)
MockUIElement("elem2", (200, 200, 30, 30)), # centre: (215, 215)
MockUIElement("elem3", (140, 140, 40, 40)), # centre: (160, 160)
]
# Position de recherche proche de elem3
search_position = (170, 170)
# Mock context avec spatial_index=None pour forcer le fallback linéaire
mock_context = Mock()
mock_context.workflow_context = {"spatial_index": None}
# Mock _get_ui_elements pour retourner nos éléments
resolver = TargetResolver(position_tolerance=50)
with patch.object(resolver, '_get_ui_elements', return_value=elements):
# Résoudre par position
result = resolver._resolve_by_position(search_position, elements, mock_context)
# Devrait trouver elem3 (distance ≈ 14)
assert result is not None
assert result.element.element_id == "elem3"
def test_target_resolver_proximity_filter():
"""Test que le filtre de proximité utilise les bons calculs de centre"""
# Élément ancre: bbox (90, 110, 20, 20) -> centre (100, 120)
anchor = MockUIElement("anchor", (90, 110, 20, 20))
# Éléments à tester (distances au centre de l'ancre (100, 120)):
# near: centre (125, 125), distance = sqrt(25² + 5²) ≈ 25.5
# medium: centre (130, 130), distance = sqrt(30² + 10²) ≈ 31.6
# far: centre (205, 205), distance = sqrt(105² + 85²) ≈ 135.1
elements = [
MockUIElement("near", (120, 120, 10, 10)),
MockUIElement("medium", (125, 125, 10, 10)),
MockUIElement("far", (200, 200, 10, 10)),
]
resolver = TargetResolver()
# Filtrer avec distance max = 50
filtered = resolver._filter_by_proximity(elements, anchor, max_distance=50)
# Seuls "near" et "medium" devraient être dans le résultat
filtered_ids = [elem.element_id for elem in filtered]
assert "near" in filtered_ids
assert "medium" in filtered_ids
assert "far" not in filtered_ids
if __name__ == "__main__":
pytest.main([__file__, "-v"])