- 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>
539 lines
18 KiB
Python
539 lines
18 KiB
Python
"""
|
|
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__]) |