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>
172 lines
6.2 KiB
Python
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"]) |