""" Tests d'intégration pour l'auto-healing Fiche #10 Auteur: Dom, Alice Kiro - 15 décembre 2024 Tests d'intégration end-to-end: - ActionExecutor + TargetResolver avec healing - Séquences multi-tentatives avec backoff - Mesure de l'impact performance - Coordination cross-component """ import time from datetime import datetime from unittest.mock import Mock, patch from core.execution.target_resolver import TargetResolver from core.execution.action_executor import ActionExecutor from core.models.workflow_graph import ( TargetSpec, WorkflowEdge, Action, PostConditions, PostConditionCheck ) from core.models.screen_state import ( ScreenState, RawLevel, PerceptionLevel, ContextLevel, WindowContext, EmbeddingRef ) from core.models.ui_element import UIElement, UIElementEmbeddings, VisualFeatures from core.execution.action_executor import ExecutionStatus def create_ui_element(eid, role, bbox, label="", etype="ui", conf=0.9): """Helper pour créer un UIElement""" return UIElement( element_id=eid, type=etype, role=role, bbox=bbox, 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 create_screen_state(elements): """Helper pour créer un ScreenState""" return ScreenState( screen_state_id="test_screen", timestamp=datetime.now(), session_id="test_session", window=WindowContext(app_name="test_app", window_title="Test Window", screen_resolution=[1920,1080]), raw=RawLevel(screenshot_path="test.png", capture_method="test", file_size_bytes=1024), perception=PerceptionLevel( embedding=EmbeddingRef(provider="test", vector_id="test_vector", dimensions=512), detected_text=[], text_detection_method="test", confidence_avg=0.9 ), context=ContextLevel(), ui_elements=elements ) class TestHealingIntegration: """Tests d'intégration du système healing complet""" def test_end_to_end_healing_with_retry(self): """Test end-to-end avec ActionExecutor et retry healing""" # Test simplifié de l'intégration ActionExecutor + TargetResolver resolver = TargetResolver() executor = ActionExecutor(target_resolver=resolver) # Vérifier l'état initial assert resolver.healing_attempt == 0 # Simuler l'activation du healing par l'executor (comme dans la boucle retry) resolver.healing_attempt = 1 # Vérifier que le profil healing est appliqué profile = resolver._healing_profile() assert profile["expand_roles"] == True assert profile["min_ratio"] == 0.78 assert profile["pad_mul"] == 1.3 # Simuler le reset par l'executor resolver.healing_attempt = 0 # Vérifier le retour au mode strict profile_reset = resolver._healing_profile() assert profile_reset["expand_roles"] == False assert profile_reset["min_ratio"] == 0.82 assert profile_reset["pad_mul"] == 1.0 def test_healing_progression_sequence(self): """Test d'une séquence de healing avec progression des tentatives""" resolver = TargetResolver() # Simuler une séquence de healing attempts healing_profiles = [] for attempt in range(4): resolver.healing_attempt = attempt profile = resolver._healing_profile() healing_profiles.append(profile) # Vérifier la progression assert healing_profiles[0]["expand_roles"] == False # Strict assert healing_profiles[1]["expand_roles"] == True # First healing assert healing_profiles[2]["expand_roles"] == True # Desperate assert healing_profiles[3]["expand_roles"] == True # Still desperate # Vérifier la progression des seuils assert healing_profiles[0]["min_ratio"] == 0.82 assert healing_profiles[1]["min_ratio"] == 0.78 assert healing_profiles[2]["min_ratio"] == 0.72 assert healing_profiles[3]["min_ratio"] == 0.72 # Reste à 0.72 # Vérifier la progression du padding assert healing_profiles[0]["pad_mul"] == 1.0 assert healing_profiles[1]["pad_mul"] == 1.3 assert healing_profiles[2]["pad_mul"] == 1.7 assert healing_profiles[3]["pad_mul"] == 1.7 # Reste à 1.7 def test_healing_with_ui_changes(self): """Test healing avec changements d'UI réalistes""" # Scénario: L'UI change entre les tentatives # État 1: Élément avec rôle strict elements_v1 = [ create_ui_element("field", "textfield", (100, 100, 200, 30), "") ] # État 2: Élément avec rôle modifié (form_input au lieu de textfield) elements_v2 = [ create_ui_element("field", "form_input", (100, 100, 200, 30), "") ] screen_v1 = create_screen_state(elements_v1) screen_v2 = create_screen_state(elements_v2) resolver = TargetResolver() spec = TargetSpec(by_role="input") # Cherche "input" # Mode strict: ne trouve ni textfield ni form_input resolver.healing_attempt = 0 result_v1_strict = resolver.resolve_target(spec, screen_v1) result_v2_strict = resolver.resolve_target(spec, screen_v2) # Mode healing: trouve avec aliases resolver.healing_attempt = 1 result_v1_healing = resolver.resolve_target(spec, screen_v1) result_v2_healing = resolver.resolve_target(spec, screen_v2) # Vérifier que le healing permet de trouver dans les deux cas assert result_v1_healing is not None assert result_v2_healing is not None assert result_v1_healing.element.element_id == "field" assert result_v2_healing.element.element_id == "field" def test_performance_impact_measurement(self): """Test de mesure de l'impact performance du healing""" resolver = TargetResolver() # Créer un écran avec plusieurs éléments elements = [ create_ui_element(f"elem_{i}", "button", (i*50, 100, 40, 30), f"Button {i}") for i in range(20) # 20 éléments ] screen = create_screen_state(elements) spec = TargetSpec(by_role="submit") # Rôle qui n'existe pas # Mesurer le temps en mode strict start_time = time.perf_counter() resolver.healing_attempt = 0 result_strict = resolver.resolve_target(spec, screen) strict_duration = time.perf_counter() - start_time # Mesurer le temps en mode healing start_time = time.perf_counter() resolver.healing_attempt = 1 result_healing = resolver.resolve_target(spec, screen) healing_duration = time.perf_counter() - start_time # Le healing ne devrait pas ajouter beaucoup d'overhead # (facteur 2x maximum acceptable pour ce test) assert healing_duration < strict_duration * 2.0 # Les deux devraient échouer car pas de "submit" assert result_strict is None assert result_healing is None def test_cross_component_healing_coordination(self): """Test de coordination healing entre ActionExecutor et TargetResolver""" resolver = TargetResolver() executor = ActionExecutor(target_resolver=resolver) # Vérifier l'état initial assert resolver.healing_attempt == 0 # Simuler l'activation du healing par l'executor resolver.healing_attempt = 2 # Vérifier que le profil est correctement appliqué profile = resolver._healing_profile() assert profile["min_ratio"] == 0.72 assert profile["expand_roles"] == True assert profile["pad_mul"] == 1.7 # Simuler le reset par l'executor resolver.healing_attempt = 0 # Vérifier le retour au mode strict profile_reset = resolver._healing_profile() assert profile_reset["min_ratio"] == 0.82 assert profile_reset["expand_roles"] == False assert profile_reset["pad_mul"] == 1.0 def test_healing_with_context_hints(self): """Test healing avec context_hints et padding spatial""" # Créer un anchor et un target anchor = create_ui_element("lbl", "label", (100, 100, 80, 20), "Username") target = create_ui_element("inp", "input", (300, 100, 150, 25), "") # Éloigné elements = [anchor, target] screen = create_screen_state(elements) resolver = TargetResolver() spec = TargetSpec( by_role="input", context_hints={"right_of_text": "Username"} ) # Test avec différents niveaux de healing for healing_level in [0, 1, 2]: resolver.healing_attempt = healing_level result = resolver.resolve_target(spec, screen) if result: # Vérifier les métadonnées de healing details = result.resolution_details assert "healing_attempt" in details assert "healing_profile" in details # Note: healing_attempt peut être 0 si la résolution réussit sans healing # Vérifier que le profil correspond au niveau attendu expected_pad_mul = [1.0, 1.3, 1.7][min(healing_level, 2)] if "spatial_padding_used" in details: # Si la résolution utilise le spatial padding, vérifier la valeur # Sinon, c'est que la résolution a réussi sans utiliser le spatial fallback if details["spatial_padding_used"] != 1.0 or healing_level == 0: assert details["spatial_padding_used"] == expected_pad_mul class TestHealingErrorScenarios: """Tests des scénarios d'erreur avec healing""" def test_healing_with_no_elements(self): """Test healing quand aucun élément n'est présent""" empty_screen = create_screen_state([]) resolver = TargetResolver() spec = TargetSpec(by_role="button") # Même avec healing, ne peut pas trouver d'éléments inexistants for healing_level in [0, 1, 2]: resolver.healing_attempt = healing_level result = resolver.resolve_target(spec, empty_screen) assert result is None def test_healing_counter_overflow(self): """Test avec des valeurs extrêmes de healing_attempt""" resolver = TargetResolver() # Tester avec des valeurs élevées for extreme_value in [10, 100, 1000]: resolver.healing_attempt = extreme_value profile = resolver._healing_profile() # Devrait rester au niveau "desperate" assert profile["min_ratio"] == 0.72 assert profile["expand_roles"] == True assert profile["pad_mul"] == 1.7 def test_healing_with_malformed_elements(self): """Test healing avec des éléments malformés""" # Créer un élément avec des attributs manquants/None malformed_element = UIElement( element_id="malformed", type=None, # Type manquant role="", # Rôle vide bbox=(0, 0, 0, 0), # Bbox invalide center=(0, 0), label=None, # Label manquant label_confidence=0.0, embeddings=UIElementEmbeddings(image=None, text=None), visual_features=VisualFeatures( dominant_color="", has_icon=False, shape="", size_category="" ), confidence=0.0, tags=[], metadata={} ) screen = create_screen_state([malformed_element]) resolver = TargetResolver() spec = TargetSpec(by_role="button") # Le healing ne devrait pas planter avec des éléments malformés resolver.healing_attempt = 1 result = resolver.resolve_target(spec, screen) # Devrait retourner None proprement assert result is None def test_healing_integration_suite(): """Suite de tests d'intégration rapide""" print("🧪 Tests d'intégration auto-healing...") # Test basique resolver = TargetResolver() assert resolver.healing_attempt == 0 # Test progression for i in range(3): resolver.healing_attempt = i profile = resolver._healing_profile() assert "min_ratio" in profile assert "pad_mul" in profile assert "expand_roles" in profile print("✅ Tests d'intégration auto-healing - Tous les tests passent!") if __name__ == "__main__": test_healing_integration_suite()