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>
This commit is contained in:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

View File

@@ -0,0 +1,539 @@
"""
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__])