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

402 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Tests Unitaires Registry Actions VWB
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Ce script teste le registry des actions VisionOnly pour le Visual Workflow Builder.
Tests :
- Création et initialisation du registry
- Enregistrement d'actions
- Recherche et récupération d'actions
- Création d'instances d'actions
- Auto-découverte des actions
- Thread-safety du registry
"""
import sys
import unittest
import threading
import time
from pathlib import Path
from typing import Dict, Any, Optional
# Ajouter le répertoire racine au path
ROOT_DIR = Path(__file__).parent.parent.parent
sys.path.insert(0, str(ROOT_DIR))
sys.path.insert(0, str(ROOT_DIR / "visual_workflow_builder" / "backend"))
try:
# Import avec chemin absolu
sys.path.insert(0, str(ROOT_DIR / "visual_workflow_builder" / "backend"))
from actions.registry import VWBActionRegistry, get_global_registry, vwb_action
from actions.base_action import BaseVWBAction, VWBActionResult, VWBActionStatus
# Essayer d'importer les actions spécifiques
try:
from actions.vision_ui.click_anchor import VWBClickAnchorAction
from actions.vision_ui.type_text import VWBTypeTextAction
from actions.vision_ui.wait_for_anchor import VWBWaitForAnchorAction
SPECIFIC_ACTIONS_OK = True
except ImportError:
SPECIFIC_ACTIONS_OK = False
print("⚠️ Actions spécifiques non disponibles")
IMPORTS_OK = True
print("✅ Imports du registry réussis")
except ImportError as e:
print(f"⚠️ Imports non disponibles: {e}")
IMPORTS_OK = False
BaseVWBAction = None
VWBActionResult = None
VWBActionStatus = None
if IMPORTS_OK and BaseVWBAction is not None:
class MockVWBAction(BaseVWBAction):
"""Action mock pour les tests."""
def __init__(self, action_id: str, parameters: Optional[Dict[str, Any]] = None, **kwargs):
super().__init__(action_id, parameters or {})
self.executed = False
def _execute_impl(self, step_id: str, workflow_id: Optional[str] = None,
user_id: Optional[str] = None) -> VWBActionResult:
"""Implémentation mock de l'exécution."""
self.executed = True
result = VWBActionResult(
action_id=self.action_id,
step_id=step_id,
status=VWBActionStatus.SUCCESS,
workflow_id=workflow_id,
user_id=user_id
)
result.output_data = {"mock": True, "executed": True}
return result
def validate_parameters(self) -> list:
"""Validation mock."""
return []
else:
MockVWBAction = None
@unittest.skipUnless(IMPORTS_OK, "Imports VWB non disponibles")
class TestVWBActionRegistry(unittest.TestCase):
"""Tests pour le registry des actions VWB."""
def setUp(self):
"""Préparation des tests."""
self.registry = VWBActionRegistry()
def tearDown(self):
"""Nettoyage après tests."""
self.registry.clear()
def test_registry_initialization(self):
"""Test de l'initialisation du registry."""
self.assertIsInstance(self.registry, VWBActionRegistry)
self.assertEqual(len(self.registry.list_actions()), 0)
self.assertEqual(len(self.registry.list_categories()), 0)
def test_register_action(self):
"""Test de l'enregistrement d'actions."""
# Enregistrer une action mock
success = self.registry.register_action(
MockVWBAction,
"mock_action",
"test",
{"description": "Action de test"}
)
self.assertTrue(success)
self.assertIn("mock_action", self.registry.list_actions())
self.assertIn("test", self.registry.list_categories())
# Vérifier les métadonnées
metadata = self.registry.get_action_metadata("mock_action")
self.assertIsNotNone(metadata)
self.assertEqual(metadata["category"], "test")
self.assertEqual(metadata["class_name"], "MockVWBAction")
def test_register_duplicate_action(self):
"""Test de l'enregistrement d'actions dupliquées."""
# Premier enregistrement
success1 = self.registry.register_action(MockVWBAction, "duplicate_test")
self.assertTrue(success1)
# Tentative de duplication
success2 = self.registry.register_action(MockVWBAction, "duplicate_test")
self.assertFalse(success2)
# Vérifier qu'il n'y a qu'une seule action
actions = self.registry.list_actions()
self.assertEqual(actions.count("duplicate_test"), 1)
def test_get_action_class(self):
"""Test de récupération de classe d'action."""
# Enregistrer une action
self.registry.register_action(MockVWBAction, "test_get_class")
# Récupérer la classe
action_class = self.registry.get_action_class("test_get_class")
self.assertEqual(action_class, MockVWBAction)
# Test avec action inexistante
non_existent = self.registry.get_action_class("non_existent")
self.assertIsNone(non_existent)
def test_create_action_instance(self):
"""Test de création d'instances d'actions."""
# Enregistrer une action
self.registry.register_action(MockVWBAction, "test_create")
# Créer une instance
instance = self.registry.create_action(
"test_create",
{"param1": "value1"}
)
self.assertIsNotNone(instance)
self.assertIsInstance(instance, MockVWBAction)
self.assertEqual(instance.parameters["param1"], "value1")
# Test avec action inexistante
non_existent = self.registry.create_action("non_existent")
self.assertIsNone(non_existent)
def test_list_actions_by_category(self):
"""Test de listage d'actions par catégorie."""
# Enregistrer des actions dans différentes catégories
self.registry.register_action(MockVWBAction, "action1", "category1")
self.registry.register_action(MockVWBAction, "action2", "category1")
self.registry.register_action(MockVWBAction, "action3", "category2")
# Tester le listage par catégorie
cat1_actions = self.registry.list_actions("category1")
self.assertEqual(len(cat1_actions), 2)
self.assertIn("action1", cat1_actions)
self.assertIn("action2", cat1_actions)
cat2_actions = self.registry.list_actions("category2")
self.assertEqual(len(cat2_actions), 1)
self.assertIn("action3", cat2_actions)
# Tester le listage de toutes les actions
all_actions = self.registry.list_actions()
self.assertEqual(len(all_actions), 3)
def test_search_actions(self):
"""Test de recherche d'actions."""
# Enregistrer des actions avec des noms différents
self.registry.register_action(MockVWBAction, "click_button", "ui")
self.registry.register_action(MockVWBAction, "type_text", "ui")
self.registry.register_action(MockVWBAction, "wait_element", "control")
# Recherche par terme
click_results = self.registry.search_actions("click")
self.assertIn("click_button", click_results)
self.assertEqual(len(click_results), 1)
# Recherche par catégorie
ui_results = self.registry.search_actions("type", "ui")
self.assertIn("type_text", ui_results)
self.assertEqual(len(ui_results), 1)
# Recherche sans résultat
no_results = self.registry.search_actions("nonexistent")
self.assertEqual(len(no_results), 0)
def test_registry_stats(self):
"""Test des statistiques du registry."""
# Registry vide
stats = self.registry.get_registry_stats()
self.assertEqual(stats["total_actions"], 0)
self.assertEqual(len(stats["categories"]), 0)
# Ajouter des actions
self.registry.register_action(MockVWBAction, "action1", "cat1")
self.registry.register_action(MockVWBAction, "action2", "cat1")
self.registry.register_action(MockVWBAction, "action3", "cat2")
# Vérifier les statistiques
stats = self.registry.get_registry_stats()
self.assertEqual(stats["total_actions"], 3)
self.assertEqual(stats["categories"]["cat1"], 2)
self.assertEqual(stats["categories"]["cat2"], 1)
def test_auto_discover_actions(self):
"""Test de la découverte automatique d'actions."""
# Note: Ce test dépend de la structure des fichiers
# Il peut échouer si les actions VWB ne sont pas disponibles
try:
discovered_count = self.registry.auto_discover_actions()
# Vérifier qu'au moins quelques actions ont été découvertes
self.assertGreaterEqual(discovered_count, 0)
# Vérifier que le registry est marqué comme initialisé
stats = self.registry.get_registry_stats()
self.assertTrue(stats["initialized"])
print(f"✅ Découverte automatique : {discovered_count} actions trouvées")
except Exception as e:
# La découverte peut échouer si les modules ne sont pas disponibles
print(f"⚠️ Découverte automatique échouée : {e}")
self.skipTest("Découverte automatique non disponible")
def test_thread_safety(self):
"""Test de la thread-safety du registry."""
results = []
errors = []
def register_actions(thread_id: int):
"""Fonction pour enregistrer des actions dans un thread."""
try:
for i in range(5):
action_id = f"thread_{thread_id}_action_{i}"
success = self.registry.register_action(
MockVWBAction,
action_id,
f"thread_{thread_id}"
)
results.append((thread_id, action_id, success))
time.sleep(0.001) # Petite pause pour simuler du travail
except Exception as e:
errors.append((thread_id, str(e)))
# Créer et lancer plusieurs threads
threads = []
for i in range(3):
thread = threading.Thread(target=register_actions, args=(i,))
threads.append(thread)
thread.start()
# Attendre la fin de tous les threads
for thread in threads:
thread.join()
# Vérifier les résultats
self.assertEqual(len(errors), 0, f"Erreurs dans les threads : {errors}")
self.assertEqual(len(results), 15) # 3 threads × 5 actions
# Vérifier que toutes les actions ont été enregistrées
all_actions = self.registry.list_actions()
self.assertEqual(len(all_actions), 15)
print(f"✅ Thread-safety validée : {len(results)} enregistrements réussis")
def test_decorator_registration(self):
"""Test de l'enregistrement via décorateur."""
@vwb_action("decorated_action", "decorator_test", {"decorated": True})
class DecoratedAction(BaseVWBAction):
def _execute_impl(self, step_id: str, workflow_id: Optional[str] = None,
user_id: Optional[str] = None) -> VWBActionResult:
result = VWBActionResult(
action_id=self.action_id,
step_id=step_id,
status=VWBActionStatus.SUCCESS,
workflow_id=workflow_id,
user_id=user_id
)
return result
def validate_parameters(self) -> list:
return []
# Vérifier que l'action a été enregistrée automatiquement
global_registry = get_global_registry()
self.assertIn("decorated_action", global_registry.list_actions())
# Vérifier les métadonnées
metadata = global_registry.get_action_metadata("decorated_action")
self.assertEqual(metadata["category"], "decorator_test")
self.assertTrue(metadata["metadata"]["decorated"])
@unittest.skipUnless(IMPORTS_OK, "Imports VWB non disponibles")
class TestGlobalRegistry(unittest.TestCase):
"""Tests pour le registry global."""
def test_global_registry_singleton(self):
"""Test du pattern singleton pour le registry global."""
registry1 = get_global_registry()
registry2 = get_global_registry()
# Vérifier que c'est la même instance
self.assertIs(registry1, registry2)
def test_global_registry_auto_discovery(self):
"""Test de la découverte automatique au premier accès."""
registry = get_global_registry()
# Le registry global devrait avoir découvert des actions automatiquement
stats = registry.get_registry_stats()
print(f"📊 Registry global - Actions: {stats['total_actions']}, Catégories: {len(stats['categories'])}")
# Afficher les actions découvertes
actions = registry.list_actions()
if actions:
print(f"🔍 Actions découvertes: {', '.join(actions[:5])}{'...' if len(actions) > 5 else ''}")
def run_tests():
"""Exécute tous les tests."""
print("=" * 60)
print(" TESTS UNITAIRES REGISTRY ACTIONS VWB")
print("=" * 60)
print("Auteur : Dom, Alice, Kiro - 09 janvier 2026")
print("")
if not IMPORTS_OK:
print("❌ Imports non disponibles - tests ignorés")
return False
# Créer la suite de tests
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Ajouter les tests
suite.addTests(loader.loadTestsFromTestCase(TestVWBActionRegistry))
suite.addTests(loader.loadTestsFromTestCase(TestGlobalRegistry))
# Exécuter les tests
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Résumé
print("")
print("=" * 60)
print(" RÉSUMÉ DES TESTS")
print("=" * 60)
print(f"📊 Tests exécutés : {result.testsRun}")
print(f"✅ Tests réussis : {result.testsRun - len(result.failures) - len(result.errors)}")
print(f"❌ Tests échoués : {len(result.failures)}")
print(f"💥 Erreurs : {len(result.errors)}")
if result.failures:
print("\n❌ ÉCHECS :")
for test, traceback in result.failures:
print(f" - {test}: {traceback.split('AssertionError: ')[-1].split('\\n')[0]}")
if result.errors:
print("\n💥 ERREURS :")
for test, traceback in result.errors:
print(f" - {test}: {traceback.split('\\n')[-2]}")
success_rate = (result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100
print(f"\n📈 Taux de succès : {success_rate:.1f}%")
return len(result.failures) == 0 and len(result.errors) == 0
if __name__ == '__main__':
success = run_tests()
sys.exit(0 if success else 1)