- Smart systray (pystray+plyer) remplace PyQt5 : notifications toast, menu dynamique avec workflows, chat "Que dois-je faire ?", icône colorée - Preflight GPU : check_machine_ready() + @pytest.mark.gpu dans conftest - Correction 63 tests cassés → 0 failed (1200 passed) - Tests VWB obsolètes déplacés vers _a_trier/ - Support qwen3-vl:8b sur GPU (remplace qwen2.5vl:3b) - fix images < 32x32 (Ollama panic) - fix force_json=False (qwen3-vl incompatible) - fix temperature 0.1 (0.0 bloque avec images) - Fix captor Windows : Key.esc, _get_key_name() - Fix LeaServerClient : check_connection, list_workflows format - deploy_windows.py : packaging propre client Windows - VWB : edges visibles (#607d8b) + fitView automatique Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
13 KiB
Python
335 lines
13 KiB
Python
"""
|
|
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
|
|
# Note: BBox exige des dimensions positives, on utilise (0,0,1,1) comme bbox
|
|
# minimale valide pour tester le healing avec des éléments "malformés"
|
|
malformed_element = UIElement(
|
|
element_id="malformed",
|
|
type=None, # Type manquant
|
|
role="", # Rôle vide
|
|
bbox=(0, 0, 1, 1), # Bbox minimale valide (dimensions > 0)
|
|
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() |