Files
rpa_vision_v3/tests/unit/test_vwb_contracts_09jan2026.py
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:23:51 +01:00

658 lines
25 KiB
Python

"""
Tests Unitaires - Contrats de Données VWB
Auteur : Dom, Alice, Kiro - 09 janvier 2026
Tests complets pour les contrats de données du Visual Workflow Builder :
- VWBActionError
- VWBEvidence
- VWBVisualAnchor
Ces tests valident la sérialisation JSON, la validation des données,
et le comportement des méthodes utilitaires.
"""
import unittest
import json
import base64
from datetime import datetime
from unittest.mock import patch, MagicMock
# Import des contrats VWB
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from visual_workflow_builder.backend.contracts.error import (
VWBActionError, VWBErrorType, VWBErrorSeverity, create_vwb_error
)
from visual_workflow_builder.backend.contracts.evidence import (
VWBEvidence, VWBEvidenceType, create_screenshot_evidence, create_interaction_evidence
)
from visual_workflow_builder.backend.contracts.visual_anchor import (
VWBVisualAnchor, VWBVisualAnchorType, create_image_anchor, create_text_anchor
)
class TestVWBActionError(unittest.TestCase):
"""Tests pour VWBActionError."""
def setUp(self):
"""Configuration des tests."""
self.timestamp = datetime.now()
self.error_data = {
'error_id': 'test_error_001',
'error_type': VWBErrorType.ELEMENT_NOT_FOUND,
'severity': VWBErrorSeverity.ERROR,
'message': 'Élément non trouvé',
'description': 'L\'élément bouton "Valider" n\'a pas été trouvé sur l\'écran',
'action_id': 'action_click_001',
'step_id': 'step_001',
'workflow_id': 'workflow_test',
'timestamp': self.timestamp,
'execution_time_ms': 1500.0,
'technical_details': {'screen_resolution': '1920x1080'},
'stack_trace': 'Traceback...',
'suggestions': ['Vérifier que l\'élément est visible'],
'retry_possible': True,
'user_id': 'user_test',
'session_id': 'session_001',
'environment': 'test'
}
def test_error_creation(self):
"""Test de création d'une erreur VWB."""
error = VWBActionError(**self.error_data)
self.assertEqual(error.error_id, 'test_error_001')
self.assertEqual(error.error_type, VWBErrorType.ELEMENT_NOT_FOUND)
self.assertEqual(error.severity, VWBErrorSeverity.ERROR)
self.assertEqual(error.message, 'Élément non trouvé')
self.assertEqual(error.action_id, 'action_click_001')
self.assertTrue(error.retry_possible)
def test_error_auto_id_generation(self):
"""Test de génération automatique de l'ID d'erreur."""
data = self.error_data.copy()
data['error_id'] = ''
error = VWBActionError(**data)
self.assertTrue(error.error_id.startswith('err_action_click_001_'))
self.assertGreater(len(error.error_id), 20)
def test_default_suggestions_generation(self):
"""Test de génération des suggestions par défaut."""
data = self.error_data.copy()
data['suggestions'] = []
error = VWBActionError(**data)
self.assertGreater(len(error.suggestions), 0)
self.assertIn('Vérifiez que l\'élément est visible à l\'écran', error.suggestions)
def test_error_serialization(self):
"""Test de sérialisation JSON."""
error = VWBActionError(**self.error_data)
# Test to_dict
error_dict = error.to_dict()
self.assertEqual(error_dict['error_type'], 'element_not_found')
self.assertEqual(error_dict['severity'], 'error')
self.assertIsInstance(error_dict['timestamp'], str)
# Test to_json
error_json = error.to_json()
self.assertIsInstance(error_json, str)
parsed = json.loads(error_json)
self.assertEqual(parsed['error_type'], 'element_not_found')
def test_error_deserialization(self):
"""Test de désérialisation JSON."""
error = VWBActionError(**self.error_data)
error_dict = error.to_dict()
# Test from_dict
restored_error = VWBActionError.from_dict(error_dict)
self.assertEqual(restored_error.error_id, error.error_id)
self.assertEqual(restored_error.error_type, error.error_type)
self.assertEqual(restored_error.severity, error.severity)
# Test from_json
error_json = error.to_json()
restored_from_json = VWBActionError.from_json(error_json)
self.assertEqual(restored_from_json.message, error.message)
def test_is_retryable(self):
"""Test de la logique de retry."""
# Erreur retryable
error = VWBActionError(**self.error_data)
self.assertTrue(error.is_retryable())
# Erreur non retryable (paramètre invalide)
data = self.error_data.copy()
data['error_type'] = VWBErrorType.PARAMETER_INVALID
error_non_retryable = VWBActionError(**data)
self.assertFalse(error_non_retryable.is_retryable())
# Erreur fatale
data = self.error_data.copy()
data['severity'] = VWBErrorSeverity.FATAL
error_fatal = VWBActionError(**data)
self.assertFalse(error_fatal.is_retryable())
def test_user_friendly_message(self):
"""Test des messages conviviaux."""
error = VWBActionError(**self.error_data)
friendly_msg = error.get_user_friendly_message()
self.assertIn('', friendly_msg)
self.assertIn('Élément non trouvé sur l\'écran', friendly_msg)
def test_create_vwb_error_utility(self):
"""Test de la fonction utilitaire create_vwb_error."""
error = create_vwb_error(
error_type=VWBErrorType.CLICK_FAILED,
message='Clic échoué',
action_id='action_001',
step_id='step_001',
severity=VWBErrorSeverity.WARNING
)
self.assertEqual(error.error_type, VWBErrorType.CLICK_FAILED)
self.assertEqual(error.severity, VWBErrorSeverity.WARNING)
self.assertEqual(error.message, 'Clic échoué')
class TestVWBEvidence(unittest.TestCase):
"""Tests pour VWBEvidence."""
def setUp(self):
"""Configuration des tests."""
self.timestamp = datetime.now()
# Créer un screenshot base64 factice
self.fake_screenshot = base64.b64encode(b'fake_image_data').decode('utf-8')
self.evidence_data = {
'evidence_id': 'ev_test_001',
'evidence_type': VWBEvidenceType.SCREENSHOT_BEFORE,
'action_id': 'action_001',
'step_id': 'step_001',
'workflow_id': 'workflow_test',
'timestamp': self.timestamp,
'execution_time_ms': 500.0,
'title': 'Capture avant clic',
'description': 'Screenshot avant l\'action de clic',
'screenshot_base64': self.fake_screenshot,
'screenshot_width': 1920,
'screenshot_height': 1080,
'highlight_box': {'x': 100, 'y': 200, 'width': 150, 'height': 50},
'data': {'confidence': 0.95},
'success': True,
'confidence_score': 0.95,
'user_id': 'user_test',
'session_id': 'session_001',
'related_evidence_ids': [],
'parent_evidence_id': None
}
def test_evidence_creation(self):
"""Test de création d'une evidence VWB."""
evidence = VWBEvidence(**self.evidence_data)
self.assertEqual(evidence.evidence_id, 'ev_test_001')
self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_BEFORE)
self.assertEqual(evidence.title, 'Capture avant clic')
self.assertTrue(evidence.success)
self.assertEqual(evidence.screenshot_width, 1920)
def test_evidence_auto_id_generation(self):
"""Test de génération automatique de l'ID d'evidence."""
data = self.evidence_data.copy()
data['evidence_id'] = ''
evidence = VWBEvidence(**data)
self.assertTrue(evidence.evidence_id.startswith('ev_action_001_'))
def test_has_screenshot(self):
"""Test de détection de screenshot."""
evidence = VWBEvidence(**self.evidence_data)
self.assertTrue(evidence.has_screenshot())
# Sans screenshot
data = self.evidence_data.copy()
data['screenshot_base64'] = None
evidence_no_screenshot = VWBEvidence(**data)
self.assertFalse(evidence_no_screenshot.has_screenshot())
def test_has_highlight(self):
"""Test de détection de zone surlignée."""
evidence = VWBEvidence(**self.evidence_data)
self.assertTrue(evidence.has_highlight())
# Sans highlight
data = self.evidence_data.copy()
data['highlight_box'] = None
evidence_no_highlight = VWBEvidence(**data)
self.assertFalse(evidence_no_highlight.has_highlight())
def test_get_screenshot_data_url(self):
"""Test de génération d'URL data pour screenshot."""
evidence = VWBEvidence(**self.evidence_data)
data_url = evidence.get_screenshot_data_url()
self.assertIsNotNone(data_url)
self.assertTrue(data_url.startswith('data:image/png;base64,'))
self.assertIn(self.fake_screenshot, data_url)
def test_evidence_serialization(self):
"""Test de sérialisation JSON."""
evidence = VWBEvidence(**self.evidence_data)
# Test to_dict
evidence_dict = evidence.to_dict()
self.assertEqual(evidence_dict['evidence_type'], 'screenshot_before')
self.assertIsInstance(evidence_dict['timestamp'], str)
# Test to_json
evidence_json = evidence.to_json()
self.assertIsInstance(evidence_json, str)
parsed = json.loads(evidence_json)
self.assertEqual(parsed['evidence_type'], 'screenshot_before')
def test_evidence_deserialization(self):
"""Test de désérialisation JSON."""
evidence = VWBEvidence(**self.evidence_data)
evidence_dict = evidence.to_dict()
# Test from_dict
restored_evidence = VWBEvidence.from_dict(evidence_dict)
self.assertEqual(restored_evidence.evidence_id, evidence.evidence_id)
self.assertEqual(restored_evidence.evidence_type, evidence.evidence_type)
# Test from_json
evidence_json = evidence.to_json()
restored_from_json = VWBEvidence.from_json(evidence_json)
self.assertEqual(restored_from_json.title, evidence.title)
def test_evidence_type_checks(self):
"""Test des vérifications de type d'evidence."""
evidence = VWBEvidence(**self.evidence_data)
self.assertTrue(evidence.is_visual_evidence())
self.assertFalse(evidence.is_interaction_evidence())
# Test avec evidence d'interaction
data = self.evidence_data.copy()
data['evidence_type'] = VWBEvidenceType.CLICK_EVIDENCE
interaction_evidence = VWBEvidence(**data)
self.assertFalse(interaction_evidence.is_visual_evidence())
self.assertTrue(interaction_evidence.is_interaction_evidence())
def test_file_size_calculation(self):
"""Test du calcul de taille de fichier."""
evidence = VWBEvidence(**self.evidence_data)
file_size = evidence.get_file_size_mb()
self.assertIsInstance(file_size, float)
self.assertGreater(file_size, 0)
def test_create_screenshot_evidence_utility(self):
"""Test de la fonction utilitaire create_screenshot_evidence."""
evidence = create_screenshot_evidence(
action_id='action_001',
step_id='step_001',
screenshot_base64=self.fake_screenshot,
title='Test screenshot'
)
self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_BEFORE)
self.assertEqual(evidence.action_id, 'action_001')
self.assertEqual(evidence.title, 'Test screenshot')
self.assertTrue(evidence.has_screenshot())
def test_create_interaction_evidence_utility(self):
"""Test de la fonction utilitaire create_interaction_evidence."""
interaction_data = {'click_x': 100, 'click_y': 200}
evidence = create_interaction_evidence(
action_id='action_001',
step_id='step_001',
evidence_type=VWBEvidenceType.CLICK_EVIDENCE,
title='Clic effectué',
interaction_data=interaction_data
)
self.assertEqual(evidence.evidence_type, VWBEvidenceType.CLICK_EVIDENCE)
self.assertEqual(evidence.data, interaction_data)
self.assertTrue(evidence.is_interaction_evidence())
class TestVWBVisualAnchor(unittest.TestCase):
"""Tests pour VWBVisualAnchor."""
def setUp(self):
"""Configuration des tests."""
self.timestamp = datetime.now()
self.fake_image = base64.b64encode(b'fake_anchor_image').decode('utf-8')
self.anchor_data = {
'anchor_id': 'anchor_test_001',
'anchor_type': VWBVisualAnchorType.IMAGE_TEMPLATE,
'name': 'Bouton Valider',
'description': 'Bouton de validation du formulaire',
'reference_image_base64': self.fake_image,
'reference_width': 120,
'reference_height': 40,
'bounding_box': {'x': 500, 'y': 300, 'width': 120, 'height': 40},
'search_criteria': {'color_tolerance': 10},
'confidence_threshold': 0.8,
'max_search_time_ms': 5000,
'retry_count': 3,
'visual_embedding': [0.1, 0.2, 0.3],
'embedding_model': 'clip-vit-base',
'created_by': 'user_test',
'created_at': self.timestamp,
'last_used_at': None,
'usage_count': 0,
'success_rate': 0.0,
'average_match_time_ms': 0.0,
'screen_resolution': (1920, 1080),
'application_context': 'web_browser',
'is_active': True,
'validation_hash': None
}
def test_anchor_creation(self):
"""Test de création d'une ancre VWB."""
anchor = VWBVisualAnchor(**self.anchor_data)
self.assertEqual(anchor.anchor_id, 'anchor_test_001')
self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.IMAGE_TEMPLATE)
self.assertEqual(anchor.name, 'Bouton Valider')
self.assertEqual(anchor.confidence_threshold, 0.8)
self.assertTrue(anchor.is_active)
self.assertIsNotNone(anchor.validation_hash)
def test_anchor_auto_id_generation(self):
"""Test de génération automatique de l'ID d'ancre."""
data = self.anchor_data.copy()
data['anchor_id'] = ''
anchor = VWBVisualAnchor(**data)
self.assertTrue(anchor.anchor_id.startswith('anchor_bouton_valider_'))
def test_has_reference_image(self):
"""Test de détection d'image de référence."""
anchor = VWBVisualAnchor(**self.anchor_data)
self.assertTrue(anchor.has_reference_image())
# Sans image
data = self.anchor_data.copy()
data['reference_image_base64'] = None
anchor_no_image = VWBVisualAnchor(**data)
self.assertFalse(anchor_no_image.has_reference_image())
def test_has_bounding_box(self):
"""Test de détection de bounding box."""
anchor = VWBVisualAnchor(**self.anchor_data)
self.assertTrue(anchor.has_bounding_box())
# Sans bounding box
data = self.anchor_data.copy()
data['bounding_box'] = None
anchor_no_bbox = VWBVisualAnchor(**data)
self.assertFalse(anchor_no_bbox.has_bounding_box())
def test_has_visual_embedding(self):
"""Test de détection d'embedding visuel."""
anchor = VWBVisualAnchor(**self.anchor_data)
self.assertTrue(anchor.has_visual_embedding())
# Sans embedding
data = self.anchor_data.copy()
data['visual_embedding'] = None
anchor_no_embedding = VWBVisualAnchor(**data)
self.assertFalse(anchor_no_embedding.has_visual_embedding())
def test_update_usage_stats(self):
"""Test de mise à jour des statistiques d'utilisation."""
anchor = VWBVisualAnchor(**self.anchor_data)
# Premier usage réussi
anchor.update_usage_stats(1000.0, True)
self.assertEqual(anchor.usage_count, 1)
self.assertEqual(anchor.success_rate, 1.0)
self.assertEqual(anchor.average_match_time_ms, 1000.0)
self.assertIsNotNone(anchor.last_used_at)
# Deuxième usage échoué
anchor.update_usage_stats(2000.0, False)
self.assertEqual(anchor.usage_count, 2)
self.assertEqual(anchor.success_rate, 0.5)
self.assertEqual(anchor.average_match_time_ms, 1500.0)
def test_reliability_checks(self):
"""Test des vérifications de fiabilité."""
anchor = VWBVisualAnchor(**self.anchor_data)
# Ancre neuve - pas encore fiable
self.assertFalse(anchor.is_reliable())
self.assertFalse(anchor.needs_optimization())
# Simuler des usages réussis
for _ in range(5):
anchor.update_usage_stats(1000.0, True)
self.assertTrue(anchor.is_reliable())
self.assertFalse(anchor.needs_optimization())
# Simuler des échecs
for _ in range(10):
anchor.update_usage_stats(8000.0, False)
self.assertFalse(anchor.is_reliable())
self.assertTrue(anchor.needs_optimization())
def test_resolution_compatibility(self):
"""Test de compatibilité de résolution."""
anchor = VWBVisualAnchor(**self.anchor_data)
# Résolution identique
self.assertTrue(anchor.is_compatible_with_resolution(1920, 1080))
# Résolution proche (dans la tolérance)
self.assertTrue(anchor.is_compatible_with_resolution(1900, 1070))
# Résolution trop différente
self.assertFalse(anchor.is_compatible_with_resolution(1280, 720))
def test_search_area_calculation(self):
"""Test du calcul de zone de recherche."""
anchor = VWBVisualAnchor(**self.anchor_data)
# Même résolution
search_area = anchor.get_search_area(1920, 1080)
self.assertEqual(search_area, anchor.bounding_box)
# Résolution différente (scaling)
search_area_scaled = anchor.get_search_area(960, 540) # Moitié
expected = {
'x': 250, # 500 / 2
'y': 150, # 300 / 2
'width': 60, # 120 / 2
'height': 20 # 40 / 2
}
self.assertEqual(search_area_scaled, expected)
def test_anchor_serialization(self):
"""Test de sérialisation JSON."""
anchor = VWBVisualAnchor(**self.anchor_data)
# Test to_dict
anchor_dict = anchor.to_dict()
self.assertEqual(anchor_dict['anchor_type'], 'image_template')
self.assertIsInstance(anchor_dict['created_at'], str)
# Test to_json
anchor_json = anchor.to_json()
self.assertIsInstance(anchor_json, str)
parsed = json.loads(anchor_json)
self.assertEqual(parsed['anchor_type'], 'image_template')
def test_anchor_deserialization(self):
"""Test de désérialisation JSON."""
anchor = VWBVisualAnchor(**self.anchor_data)
anchor_dict = anchor.to_dict()
# Test from_dict
restored_anchor = VWBVisualAnchor.from_dict(anchor_dict)
self.assertEqual(restored_anchor.anchor_id, anchor.anchor_id)
self.assertEqual(restored_anchor.anchor_type, anchor.anchor_type)
# Test from_json
anchor_json = anchor.to_json()
restored_from_json = VWBVisualAnchor.from_json(anchor_json)
self.assertEqual(restored_from_json.name, anchor.name)
def test_create_image_anchor_utility(self):
"""Test de la fonction utilitaire create_image_anchor."""
anchor = create_image_anchor(
name='Test Button',
reference_image_base64=self.fake_image,
created_by='user_test',
confidence_threshold=0.9
)
self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.IMAGE_TEMPLATE)
self.assertEqual(anchor.name, 'Test Button')
self.assertEqual(anchor.confidence_threshold, 0.9)
self.assertTrue(anchor.has_reference_image())
def test_create_text_anchor_utility(self):
"""Test de la fonction utilitaire create_text_anchor."""
anchor = create_text_anchor(
name='Submit Text',
text_pattern='Valider',
created_by='user_test'
)
self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.TEXT_EXACT)
self.assertEqual(anchor.name, 'Submit Text')
self.assertEqual(anchor.search_criteria['text_pattern'], 'Valider')
class TestContractsIntegration(unittest.TestCase):
"""Tests d'intégration entre les contrats VWB."""
def test_error_evidence_relationship(self):
"""Test de relation entre erreur et evidence."""
# Créer une erreur
error = create_vwb_error(
error_type=VWBErrorType.ELEMENT_NOT_FOUND,
message='Élément non trouvé',
action_id='action_001',
step_id='step_001'
)
# Créer une evidence d'erreur liée
evidence = create_screenshot_evidence(
action_id=error.action_id,
step_id=error.step_id,
screenshot_base64=base64.b64encode(b'error_screenshot').decode('utf-8'),
evidence_type=VWBEvidenceType.SCREENSHOT_ERROR,
title='Screenshot d\'erreur'
)
# Vérifier la cohérence
self.assertEqual(error.action_id, evidence.action_id)
self.assertEqual(error.step_id, evidence.step_id)
self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_ERROR)
def test_anchor_evidence_workflow(self):
"""Test de workflow ancre → evidence."""
# Créer une ancre
anchor = create_image_anchor(
name='Login Button',
reference_image_base64=base64.b64encode(b'button_image').decode('utf-8'),
created_by='user_test'
)
# Simuler une utilisation réussie avec evidence
evidence = create_interaction_evidence(
action_id='click_login',
step_id='step_001',
evidence_type=VWBEvidenceType.CLICK_EVIDENCE,
title='Clic sur bouton login',
interaction_data={
'anchor_id': anchor.anchor_id,
'click_coordinates': {'x': 100, 'y': 200},
'match_confidence': 0.95
}
)
# Mettre à jour les stats de l'ancre
anchor.update_usage_stats(1200.0, True)
# Vérifications
self.assertEqual(evidence.data['anchor_id'], anchor.anchor_id)
self.assertEqual(anchor.usage_count, 1)
self.assertEqual(anchor.success_rate, 1.0)
self.assertTrue(evidence.success)
if __name__ == '__main__':
# Configuration des tests
unittest.TestCase.maxDiff = None
# Exécution des tests
print("=" * 70)
print(" TESTS UNITAIRES - CONTRATS DE DONNÉES VWB")
print("=" * 70)
print("Auteur : Dom, Alice, Kiro - 09 janvier 2026")
print("")
# Créer la suite de tests
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Ajouter les classes de tests
suite.addTests(loader.loadTestsFromTestCase(TestVWBActionError))
suite.addTests(loader.loadTestsFromTestCase(TestVWBEvidence))
suite.addTests(loader.loadTestsFromTestCase(TestVWBVisualAnchor))
suite.addTests(loader.loadTestsFromTestCase(TestContractsIntegration))
# Exécuter les tests
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Résumé
print("\n" + "=" * 70)
print(f"RÉSUMÉ DES TESTS CONTRATS VWB")
print("=" * 70)
print(f"Tests exécutés : {result.testsRun}")
print(f"Succès : {result.testsRun - len(result.failures) - len(result.errors)}")
print(f"Échecs : {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}")
if result.errors:
print("\nERREURS :")
for test, traceback in result.errors:
print(f"- {test}: {traceback}")
success_rate = ((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun) * 100
print(f"\nTaux de succès : {success_rate:.1f}%")
if success_rate == 100.0:
print("🎉 TOUS LES TESTS CONTRATS VWB SONT RÉUSSIS !")
else:
print("⚠️ Certains tests ont échoué - vérification nécessaire")