#!/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)