""" Tests pour la standardisation des contrats de données - Tâche 4 Valide que les nouveaux contrats de données fonctionnent correctement : - BBox : Format exclusif (x, y, width, height) avec validation Pydantic - Timestamp : Objets datetime uniquement - IDs : Strings uniquement avec validation - Migration automatique des anciens formats Auteur : Dom, Alice Kiro Date : 20 décembre 2024 """ import pytest from datetime import datetime from typing import Tuple import uuid from core.models.base_models import BBox, Timestamp, StandardID, DataConverter from core.models import UIElement, ScreenState, UIElementEmbeddings, VisualFeatures from pydantic import ValidationError class TestBBoxStandardization: """Tests pour la standardisation BBox""" def test_bbox_creation_valid(self): """Test création BBox avec données valides""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.x == 10 assert bbox.y == 20 assert bbox.width == 100 assert bbox.height == 50 def test_bbox_validation_negative_coordinates(self): """Test validation des coordonnées négatives""" with pytest.raises(ValidationError): BBox(x=-10, y=20, width=100, height=50) with pytest.raises(ValidationError): BBox(x=10, y=-20, width=100, height=50) def test_bbox_validation_zero_dimensions(self): """Test validation des dimensions nulles""" with pytest.raises(ValidationError): BBox(x=10, y=20, width=0, height=50) with pytest.raises(ValidationError): BBox(x=10, y=20, width=100, height=0) def test_bbox_from_tuple(self): """Test création depuis tuple""" bbox = BBox.from_tuple((10, 20, 100, 50)) assert bbox.x == 10 assert bbox.y == 20 assert bbox.width == 100 assert bbox.height == 50 def test_bbox_to_tuple(self): """Test conversion vers tuple""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.to_tuple() == (10, 20, 100, 50) def test_bbox_from_xyxy(self): """Test conversion depuis format (x1, y1, x2, y2)""" bbox = BBox.from_xyxy(10, 20, 110, 70) assert bbox.x == 10 assert bbox.y == 20 assert bbox.width == 100 assert bbox.height == 50 def test_bbox_to_xyxy(self): """Test conversion vers format (x1, y1, x2, y2)""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.to_xyxy() == (10, 20, 110, 70) def test_bbox_center(self): """Test calcul du centre""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.center() == (60, 45) def test_bbox_area(self): """Test calcul de l'aire""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.area() == 5000 def test_bbox_contains_point(self): """Test vérification si un point est dans la bbox""" bbox = BBox(x=10, y=20, width=100, height=50) assert bbox.contains_point(50, 40) == True assert bbox.contains_point(5, 40) == False assert bbox.contains_point(50, 10) == False def test_bbox_intersects(self): """Test intersection entre bboxes""" bbox1 = BBox(x=10, y=20, width=100, height=50) bbox2 = BBox(x=50, y=40, width=100, height=50) bbox3 = BBox(x=200, y=200, width=50, height=50) assert bbox1.intersects(bbox2) == True assert bbox1.intersects(bbox3) == False def test_bbox_intersection(self): """Test calcul de l'intersection""" bbox1 = BBox(x=10, y=20, width=100, height=50) bbox2 = BBox(x=50, y=40, width=100, height=50) intersection = bbox1.intersection(bbox2) assert intersection is not None assert intersection.x == 50 assert intersection.y == 40 assert intersection.width == 60 assert intersection.height == 30 def test_bbox_union(self): """Test calcul de l'union""" bbox1 = BBox(x=10, y=20, width=100, height=50) bbox2 = BBox(x=50, y=40, width=100, height=50) union = bbox1.union(bbox2) assert union.x == 10 assert union.y == 20 assert union.width == 140 assert union.height == 70 class TestTimestampStandardization: """Tests pour la standardisation Timestamp""" def test_timestamp_creation_datetime(self): """Test création avec datetime""" dt = datetime.now() ts = Timestamp(value=dt) assert ts.value == dt def test_timestamp_creation_string(self): """Test création avec string ISO""" iso_str = "2024-12-20T10:30:00" ts = Timestamp(value=iso_str) assert ts.value == datetime.fromisoformat(iso_str) def test_timestamp_creation_unix(self): """Test création avec timestamp Unix""" unix_ts = 1703073000.0 ts = Timestamp(value=unix_ts) assert ts.value == datetime.fromtimestamp(unix_ts) def test_timestamp_validation_invalid(self): """Test validation avec type invalide""" with pytest.raises(ValidationError): Timestamp(value=[1, 2, 3]) def test_timestamp_to_iso(self): """Test conversion vers ISO""" dt = datetime(2024, 12, 20, 10, 30, 0) ts = Timestamp(value=dt) assert ts.to_iso() == "2024-12-20T10:30:00" def test_timestamp_to_timestamp(self): """Test conversion vers timestamp Unix""" dt = datetime(2024, 12, 20, 10, 30, 0) ts = Timestamp(value=dt) assert abs(ts.to_timestamp() - dt.timestamp()) < 0.001 def test_timestamp_now(self): """Test création timestamp maintenant""" ts = Timestamp.now() assert isinstance(ts.value, datetime) assert abs((datetime.now() - ts.value).total_seconds()) < 1 def test_timestamp_from_iso(self): """Test création depuis ISO""" iso_str = "2024-12-20T10:30:00" ts = Timestamp.from_iso(iso_str) assert ts.value == datetime.fromisoformat(iso_str) def test_timestamp_from_timestamp(self): """Test création depuis timestamp Unix""" unix_ts = 1703073000.0 ts = Timestamp.from_timestamp(unix_ts) assert ts.value == datetime.fromtimestamp(unix_ts) class TestStandardIDValidation: """Tests pour la standardisation des IDs""" def test_id_creation_string(self): """Test création avec string""" id_obj = StandardID(value="test_id_123") assert id_obj.value == "test_id_123" def test_id_creation_number(self): """Test création avec nombre""" id_obj = StandardID(value=12345) assert id_obj.value == "12345" def test_id_creation_uuid(self): """Test création avec UUID""" uuid_obj = uuid.uuid4() id_obj = StandardID(value=uuid_obj) assert id_obj.value == str(uuid_obj) def test_id_validation_empty(self): """Test validation ID vide""" with pytest.raises(ValidationError): StandardID(value="") with pytest.raises(ValidationError): StandardID(value=" ") def test_id_validation_invalid_type(self): """Test validation type invalide""" with pytest.raises(ValidationError): StandardID(value=[1, 2, 3]) def test_id_string_conversion(self): """Test conversion vers string""" id_obj = StandardID(value="test_id") assert str(id_obj) == "test_id" def test_id_equality(self): """Test égalité des IDs""" id1 = StandardID(value="test_id") id2 = StandardID(value="test_id") id3 = StandardID(value="other_id") assert id1 == id2 assert id1 != id3 assert id1 == "test_id" assert id1 != "other_id" def test_id_hash(self): """Test hash des IDs""" id1 = StandardID(value="test_id") id2 = StandardID(value="test_id") assert hash(id1) == hash(id2) assert hash(id1) == hash("test_id") def test_id_generate(self): """Test génération d'ID unique""" id1 = StandardID.generate() id2 = StandardID.generate() assert isinstance(id1.value, str) assert isinstance(id2.value, str) assert id1 != id2 assert len(id1.value) == 36 # UUID format def test_id_from_uuid(self): """Test création depuis UUID""" uuid_obj = uuid.uuid4() id_obj = StandardID.from_uuid(uuid_obj) assert id_obj.value == str(uuid_obj) class TestDataConverter: """Tests pour les utilitaires de conversion""" def test_ensure_bbox_from_bbox(self): """Test ensure_bbox avec BBox existante""" original = BBox(x=10, y=20, width=100, height=50) result = DataConverter.ensure_bbox(original) assert result == original def test_ensure_bbox_from_tuple(self): """Test ensure_bbox depuis tuple""" result = DataConverter.ensure_bbox((10, 20, 100, 50)) assert isinstance(result, BBox) assert result.x == 10 assert result.y == 20 assert result.width == 100 assert result.height == 50 def test_ensure_bbox_from_dict_xywh(self): """Test ensure_bbox depuis dict (x,y,w,h)""" result = DataConverter.ensure_bbox({ 'x': 10, 'y': 20, 'width': 100, 'height': 50 }) assert isinstance(result, BBox) assert result.x == 10 assert result.y == 20 assert result.width == 100 assert result.height == 50 def test_ensure_bbox_from_dict_xyxy(self): """Test ensure_bbox depuis dict (x1,y1,x2,y2)""" result = DataConverter.ensure_bbox({ 'x1': 10, 'y1': 20, 'x2': 110, 'y2': 70 }) assert isinstance(result, BBox) assert result.x == 10 assert result.y == 20 assert result.width == 100 assert result.height == 50 def test_ensure_bbox_invalid(self): """Test ensure_bbox avec type invalide""" with pytest.raises(ValueError): DataConverter.ensure_bbox("invalid") def test_ensure_timestamp_from_timestamp(self): """Test ensure_timestamp avec Timestamp existant""" original = Timestamp.now() result = DataConverter.ensure_timestamp(original) assert result == original def test_ensure_timestamp_from_datetime(self): """Test ensure_timestamp depuis datetime""" dt = datetime.now() result = DataConverter.ensure_timestamp(dt) assert isinstance(result, Timestamp) assert result.value == dt def test_ensure_id_from_id(self): """Test ensure_id avec StandardID existant""" original = StandardID(value="test_id") result = DataConverter.ensure_id(original) assert result == original def test_ensure_id_from_string(self): """Test ensure_id depuis string""" result = DataConverter.ensure_id("test_id") assert isinstance(result, StandardID) assert result.value == "test_id" def test_migrate_bbox_dict(self): """Test migration des bbox dans un dictionnaire""" data = { 'bbox': (10, 20, 100, 50), 'other_field': 'value' } result = DataConverter.migrate_bbox_dict(data) assert 'bbox' in result assert isinstance(result['bbox'], dict) assert result['bbox']['x'] == 10 assert result['bbox']['y'] == 20 assert result['bbox']['width'] == 100 assert result['bbox']['height'] == 50 assert result['other_field'] == 'value' def test_migrate_timestamp_dict(self): """Test migration des timestamps dans un dictionnaire""" data = { 'timestamp': '2024-12-20T10:30:00', 'other_field': 'value' } result = DataConverter.migrate_timestamp_dict(data) assert 'timestamp' in result assert isinstance(result['timestamp'], datetime) assert result['other_field'] == 'value' def test_migrate_id_dict(self): """Test migration des IDs dans un dictionnaire""" data = { 'element_id': 12345, 'other_field': 'value' } result = DataConverter.migrate_id_dict(data) assert 'element_id' in result assert isinstance(result['element_id'], str) assert result['element_id'] == '12345' assert result['other_field'] == 'value' class TestUIElementMigration: """Tests pour la migration UIElement vers nouveaux contrats""" def create_sample_embeddings(self): """Créer des embeddings de test""" return UIElementEmbeddings( image={'vector': [0.1, 0.2, 0.3]}, text={'vector': [0.4, 0.5, 0.6]} ) def create_sample_visual_features(self): """Créer des features visuelles de test""" return VisualFeatures( dominant_color="blue", has_icon=True, shape="rectangle", size_category="medium" ) def test_uielement_creation_new_format(self): """Test création UIElement avec nouveaux contrats""" bbox = BBox(x=10, y=20, width=100, height=50) element = UIElement( element_id="test_element", type="button", role="primary_action", bbox=bbox, center=(60, 45), label="Test Button", label_confidence=0.9, embeddings=self.create_sample_embeddings(), visual_features=self.create_sample_visual_features() ) assert element.element_id == "test_element" assert isinstance(element.bbox, BBox) assert element.bbox.x == 10 assert element.center == (60, 45) def test_uielement_creation_with_tuple_bbox(self): """Test création UIElement avec bbox tuple (migration automatique)""" element = UIElement.create_with_bbox_tuple( element_id="test_element", type="button", role="primary_action", bbox_tuple=(10, 20, 100, 50), label="Test Button", label_confidence=0.9, embeddings=self.create_sample_embeddings(), visual_features=self.create_sample_visual_features() ) assert isinstance(element.bbox, BBox) assert element.bbox.x == 10 assert element.bbox.y == 20 assert element.bbox.width == 100 assert element.bbox.height == 50 assert element.center == (60, 45) def test_uielement_serialization(self): """Test sérialisation UIElement""" bbox = BBox(x=10, y=20, width=100, height=50) element = UIElement( element_id="test_element", type="button", role="primary_action", bbox=bbox, center=(60, 45), label="Test Button", label_confidence=0.9, embeddings=self.create_sample_embeddings(), visual_features=self.create_sample_visual_features() ) data = element.to_dict() assert data['element_id'] == "test_element" assert isinstance(data['bbox'], dict) assert data['bbox']['x'] == 10 assert data['bbox']['y'] == 20 assert data['bbox']['width'] == 100 assert data['bbox']['height'] == 50 def test_uielement_deserialization_legacy(self): """Test désérialisation UIElement depuis format legacy""" legacy_data = { 'element_id': 12345, # Numérique (legacy) 'type': 'button', 'role': 'primary_action', 'bbox': [10, 20, 100, 50], # Liste (legacy) 'center': [60, 45], 'label': 'Test Button', 'label_confidence': 0.9, 'embeddings': { 'image': {'vector': [0.1, 0.2, 0.3]}, 'text': {'vector': [0.4, 0.5, 0.6]} }, 'visual_features': { 'dominant_color': 'blue', 'has_icon': True, 'shape': 'rectangle', 'size_category': 'medium' } } element = UIElement.from_dict(legacy_data) assert element.element_id == "12345" # Migré vers string assert isinstance(element.bbox, BBox) assert element.bbox.x == 10 assert element.bbox.y == 20 assert element.bbox.width == 100 assert element.bbox.height == 50 class TestScreenStateMigration: """Tests pour la migration ScreenState vers nouveaux contrats""" def test_screenstate_id_migration(self): """Test migration des IDs dans ScreenState""" from core.models.screen_state import WindowContext, RawLevel, PerceptionLevel, ContextLevel, EmbeddingRef # Créer les composants nécessaires window = WindowContext( app_name="TestApp", window_title="Test Window", screen_resolution=[1920, 1080] ) raw = RawLevel( screenshot_path="/path/to/screenshot.png", capture_method="mss", file_size_bytes=1024 ) embedding_ref = EmbeddingRef( provider="test_provider", vector_id="test_vector", dimensions=512 ) perception = PerceptionLevel( embedding=embedding_ref, detected_text=["Test text"], text_detection_method="test_method", confidence_avg=0.9 ) context = ContextLevel( user_id=12345 # Numérique (legacy) ) screen_state = ScreenState( screen_state_id=67890, # Numérique (legacy) timestamp="2024-12-20T10:30:00", # String (legacy) session_id="session_123", window=window, raw=raw, perception=perception, context=context ) # Vérifier que les migrations ont eu lieu assert isinstance(screen_state.screen_state_id, str) assert screen_state.screen_state_id == "67890" assert isinstance(screen_state.timestamp, datetime) assert isinstance(screen_state.session_id, str) if __name__ == "__main__": pytest.main([__file__])