Files
rpa_vision_v3/tests/unit/test_faiss_reindex_real.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

505 lines
19 KiB
Python

"""
Tests unitaires pour FAISS Rebuild Propre - Real Functionality Tests
Auteur : Dom, Alice Kiro - 22 décembre 2025
Tests pour les nouvelles fonctionnalités avec vraies implémentations:
- FAISSManager.clear() amélioré
- FAISSManager.reindex() nouveau
- WorkflowPipeline._extract_node_vector() multi-version
Focus sur les tests de fonctionnalité réelle sans simulation.
"""
import pytest
import numpy as np
import tempfile
import shutil
from pathlib import Path
from datetime import datetime
from core.embedding.faiss_manager import FAISSManager
from core.pipeline.workflow_pipeline import WorkflowPipeline
from core.models.workflow_graph import (
Workflow, WorkflowNode, ScreenTemplate, WindowConstraint,
TextConstraint, UIConstraint, EmbeddingPrototype
)
class TestFAISSManagerClearReal:
"""Tests pour la méthode clear() avec vraies instances"""
def setup_method(self):
"""Setup avec répertoire temporaire réel"""
self.temp_dir = Path(tempfile.mkdtemp())
def teardown_method(self):
"""Cleanup du répertoire temporaire"""
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def test_faiss_clear_resets_state_flat_real(self):
"""Test que clear() reset complètement l'état pour index Flat avec vraies données"""
manager = FAISSManager(dimensions=128, index_type="Flat")
# Ajouter de vrais embeddings avec vraies métadonnées
vector1 = np.random.rand(128).astype(np.float32)
vector2 = np.random.rand(128).astype(np.float32)
manager.add_embedding("test1", vector1, {"workflow_id": "wf1", "node_id": "node1"})
manager.add_embedding("test2", vector2, {"workflow_id": "wf1", "node_id": "node2"})
# Vérifier état avant clear
assert manager.index.ntotal == 2
assert len(manager.metadata_store) == 2
assert manager.next_id == 2
assert manager.is_trained == True # Flat est toujours trained
# Vérifier que les embeddings sont recherchables
results = manager.search_similar(vector1, k=1)
assert len(results) == 1
assert results[0].embedding_id == "test1"
# Clear
manager.clear()
# Vérifier reset complet
assert manager.index.ntotal == 0
assert len(manager.metadata_store) == 0
assert manager.next_id == 0
assert len(manager.training_vectors) == 0
assert manager.is_trained == True # Flat reste trained
# Vérifier que la recherche ne retourne plus rien
results = manager.search_similar(vector1, k=1)
assert len(results) == 0
def test_faiss_clear_resets_state_ivf_real(self):
"""Test que clear() reset complètement l'état IVF training avec vraies données"""
manager = FAISSManager(dimensions=128, index_type="IVF")
# Ajouter quelques vecteurs pour déclencher l'entraînement
for i in range(5):
vector = np.random.rand(128).astype(np.float32)
manager.add_embedding(f"test_{i}", vector, {"index": i})
# Vérifier état avant clear
initial_training_count = len(manager.training_vectors)
initial_next_id = manager.next_id
# Clear
manager.clear()
# Vérifier reset complet IVF
assert manager.index.ntotal == 0
assert len(manager.metadata_store) == 0
assert manager.next_id == 0
assert len(manager.training_vectors) == 0
assert manager.is_trained == False # IVF pas trained après clear
# Vérifier qu'on peut recommencer à ajouter des vecteurs
new_vector = np.random.rand(128).astype(np.float32)
manager.add_embedding("new_test", new_vector, {"type": "new"})
assert manager.next_id == 1
assert len(manager.training_vectors) == 1
class TestFAISSManagerReindexReal:
"""Tests pour la méthode reindex() avec vraies données"""
def test_faiss_reindex_flat_removes_old_entries_real(self):
"""Test que reindex() supprime complètement les anciennes entrées avec vraies données"""
manager = FAISSManager(dimensions=128, index_type="Flat")
# Ajouter des embeddings initiaux réels
old_vector1 = np.random.rand(128).astype(np.float32)
old_vector2 = np.random.rand(128).astype(np.float32)
manager.add_embedding("old1", old_vector1, {"type": "old", "workflow_id": "old_wf"})
manager.add_embedding("old2", old_vector2, {"type": "old", "workflow_id": "old_wf"})
assert manager.index.ntotal == 2
# Vérifier que les anciens embeddings sont recherchables
old_results = manager.search_similar(old_vector1, k=1)
assert len(old_results) == 1
assert old_results[0].embedding_id == "old1"
# Préparer nouveaux items réels
new_vector1 = np.random.rand(128).astype(np.float32)
new_vector2 = np.random.rand(128).astype(np.float32)
items = [
("new1", new_vector1, {"type": "new", "workflow_id": "new_wf"}),
("new2", new_vector2, {"type": "new", "workflow_id": "new_wf"})
]
# Reindex
count = manager.reindex(items, force_train_ivf=False)
# Vérifier résultats
assert count == 2
assert manager.index.ntotal == 2
assert len(manager.metadata_store) == 2
# Vérifier que seules les nouvelles métadonnées sont présentes
for meta in manager.metadata_store.values():
assert meta["metadata"]["type"] == "new"
assert meta["metadata"]["workflow_id"] == "new_wf"
# Vérifier que les anciens embeddings ne sont plus recherchables
old_results = manager.search_similar(old_vector1, k=5)
old_ids = [r.embedding_id for r in old_results]
assert "old1" not in old_ids
assert "old2" not in old_ids
# Vérifier que les nouveaux embeddings sont recherchables
new_results = manager.search_similar(new_vector1, k=1)
assert len(new_results) == 1
assert new_results[0].embedding_id == "new1"
def test_faiss_reindex_ivf_trains_with_real_data(self):
"""Test que reindex() entraîne réellement l'IVF avec de vraies données"""
manager = FAISSManager(dimensions=128, index_type="IVF")
# Préparer dataset réel (petit mais suffisant pour test)
items = []
vectors = []
for i in range(10):
vector = np.random.rand(128).astype(np.float32)
vectors.append(vector)
items.append((f"item_{i}", vector, {"index": i, "workflow_id": "test_wf"}))
# Vérifier état initial
assert not manager.is_trained
assert manager.index.ntotal == 0
# Reindex avec force training
count = manager.reindex(items, force_train_ivf=True)
# Vérifier que l'entraînement a eu lieu
assert count == 10
assert manager.is_trained
assert manager.index.ntotal == 10
# Vérifier que la recherche fonctionne après entraînement
query_vector = vectors[0]
results = manager.search_similar(query_vector, k=3)
assert len(results) > 0
# Le premier résultat devrait être le vecteur lui-même (ou très proche)
best_result = results[0]
assert best_result.embedding_id == "item_0"
assert best_result.similarity > 0.95 # Très haute similarité avec lui-même
def test_faiss_reindex_handles_invalid_vectors_gracefully(self):
"""Test que reindex() ignore gracieusement les vecteurs invalides"""
manager = FAISSManager(dimensions=128, index_type="Flat")
# Mélanger vecteurs valides et invalides
valid_vector1 = np.random.rand(128).astype(np.float32)
valid_vector2 = np.random.rand(128).astype(np.float32)
items = [
("valid1", valid_vector1, {"type": "valid"}),
("invalid1", None, {"type": "invalid"}), # None vector
("valid2", valid_vector2, {"type": "valid"}),
("invalid2", None, {"type": "invalid"}), # None vector
("invalid3", np.array([1, 2, 3]), {"type": "invalid"}) # Wrong dimensions
]
# Reindex
count = manager.reindex(items)
# Vérifier que seuls les vecteurs valides ont été indexés
assert count == 2
assert manager.index.ntotal == 2
assert len(manager.metadata_store) == 2
# Vérifier que les vecteurs valides sont recherchables
results = manager.search_similar(valid_vector1, k=5)
valid_ids = [r.embedding_id for r in results]
assert "valid1" in valid_ids
assert "valid2" in valid_ids
assert "invalid1" not in valid_ids
assert "invalid2" not in valid_ids
class TestWorkflowPipelineExtractNodeVectorReal:
"""Tests pour _extract_node_vector() avec vraies instances de modèles"""
def setup_method(self):
"""Setup avec répertoire temporaire pour fichiers"""
self.temp_dir = Path(tempfile.mkdtemp())
self.pipeline = WorkflowPipeline(data_dir=str(self.temp_dir))
def teardown_method(self):
"""Cleanup du répertoire temporaire"""
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def _create_real_node_v1_format(self, embedding_list: list) -> WorkflowNode:
"""Créer un vrai WorkflowNode avec format v1 (liste directe)"""
embedding_proto = EmbeddingPrototype(
provider="test_provider",
vector_id="", # Pas utilisé en v1
min_cosine_similarity=0.8,
sample_count=1
)
template = ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=embedding_proto
)
# Ajouter la liste directement au template
template.embedding_prototype = embedding_list
node = WorkflowNode(
node_id="test_node_v1",
name="Test Node V1",
description="Test node with v1 format",
template=template
)
return node
def _create_real_node_v2_format(self, vector_file_path: str) -> WorkflowNode:
"""Créer un vrai WorkflowNode avec format v2 (fichier sur disque)"""
embedding_proto = EmbeddingPrototype(
provider="test_provider",
vector_id=vector_file_path,
min_cosine_similarity=0.8,
sample_count=1
)
template = ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=embedding_proto
)
node = WorkflowNode(
node_id="test_node_v2",
name="Test Node V2",
description="Test node with v2 format",
template=template
)
return node
def test_extract_node_vector_v1_list_format_real(self):
"""Test extraction vecteur format v1 avec vraie instance WorkflowNode"""
# Créer vrai node avec embedding en liste
test_embedding = [0.1, 0.2, 0.3, 0.4]
node = self._create_real_node_v1_format(test_embedding)
# Extraire vecteur
vector = self.pipeline._extract_node_vector(node)
# Vérifier résultat
assert vector is not None
assert isinstance(vector, np.ndarray)
assert vector.dtype == np.float32
assert len(vector) == 4
assert np.allclose(vector, test_embedding)
def test_extract_node_vector_v2_file_format_real(self):
"""Test extraction vecteur format v2 avec vrai fichier sur disque"""
# Créer fichier temporaire avec vecteur réel
test_vector = np.array([0.5, 0.6, 0.7, 0.8], dtype=np.float32)
vector_file = self.temp_dir / "test_vector.npy"
np.save(vector_file, test_vector)
# Créer vrai node avec référence fichier
node = self._create_real_node_v2_format(str(vector_file))
# Extraire vecteur
vector = self.pipeline._extract_node_vector(node)
# Vérifier résultat
assert vector is not None
assert isinstance(vector, np.ndarray)
assert vector.dtype == np.float32
assert np.allclose(vector, test_vector)
def test_extract_node_vector_graceful_failure_real(self):
"""Test que _extract_node_vector() retourne None gracieusement avec vraies instances"""
# Test avec node sans template
node_no_template = WorkflowNode(
node_id="no_template",
name="No Template",
description="Node without template",
template=None
)
vector = self.pipeline._extract_node_vector(node_no_template)
assert vector is None
# Test avec template mais pas d'embedding
template_no_embedding = ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=EmbeddingPrototype(
provider="none",
vector_id="",
min_cosine_similarity=0.8,
sample_count=0
)
)
node_no_embedding = WorkflowNode(
node_id="no_embedding",
name="No Embedding",
description="Node without embedding",
template=template_no_embedding
)
vector2 = self.pipeline._extract_node_vector(node_no_embedding)
assert vector2 is None
# Test avec fichier inexistant
node_bad_file = self._create_real_node_v2_format("/path/that/does/not/exist.npy")
vector3 = self.pipeline._extract_node_vector(node_bad_file)
assert vector3 is None
class TestWorkflowPipelineIndexWorkflowEmbeddingsReal:
"""Tests pour _index_workflow_embeddings() avec vraies données intégrées"""
def setup_method(self):
"""Setup avec répertoire temporaire et pipeline réel"""
self.temp_dir = Path(tempfile.mkdtemp())
self.pipeline = WorkflowPipeline(data_dir=str(self.temp_dir))
def teardown_method(self):
"""Cleanup du répertoire temporaire"""
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def _create_real_workflow_with_nodes(self) -> Workflow:
"""Créer un vrai workflow avec plusieurs nodes réels"""
# Créer nodes avec vrais embeddings
node1 = WorkflowNode(
node_id="node1",
name="Node 1",
description="First node",
template=ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=EmbeddingPrototype(
provider="test",
vector_id="",
min_cosine_similarity=0.8,
sample_count=1
)
)
)
node1.template.embedding_prototype = [0.1, 0.2, 0.3]
node2 = WorkflowNode(
node_id="node2",
name="Node 2",
description="Second node",
template=ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=EmbeddingPrototype(
provider="test",
vector_id="",
min_cosine_similarity=0.8,
sample_count=1
)
)
)
node2.template.embedding_prototype = [0.4, 0.5, 0.6]
# Node sans vecteur (pour tester le filtrage)
node3 = WorkflowNode(
node_id="node3",
name="Node 3",
description="Node without vector",
template=ScreenTemplate(
window=WindowConstraint(),
text=TextConstraint(),
ui=UIConstraint(),
embedding=EmbeddingPrototype(
provider="none",
vector_id="",
min_cosine_similarity=0.8,
sample_count=0
)
)
)
# Créer workflow réel
workflow = Workflow(
workflow_id="test_workflow",
name="Test Workflow",
description="Test workflow for indexing",
nodes=[node1, node2, node3],
edges=[],
learning_state="OBSERVATION",
created_at=datetime.now()
)
return workflow
def test_index_workflow_embeddings_completeness_real(self):
"""Test que tous les vecteurs valides sont extraits et indexés avec vraies données"""
# Créer workflow réel avec nodes réels
workflow = self._create_real_workflow_with_nodes()
# Vérifier état initial du FAISS
assert self.pipeline.faiss_manager.index.ntotal == 0
# Indexer les embeddings
self.pipeline._index_workflow_embeddings(workflow)
# Vérifier que les embeddings ont été indexés
assert self.pipeline.faiss_manager.index.ntotal == 2 # Seulement les 2 nodes avec vecteurs
# Vérifier que les métadonnées sont correctes
metadata_store = self.pipeline.faiss_manager.metadata_store
assert len(metadata_store) == 2
# Vérifier les métadonnées spécifiques
found_node1 = False
found_node2 = False
for meta in metadata_store.values():
embedding_id = meta["embedding_id"]
metadata = meta["metadata"]
if embedding_id == "node1":
found_node1 = True
assert metadata["workflow_id"] == "test_workflow"
assert metadata["node_id"] == "node1"
assert metadata["node_name"] == "Node 1"
elif embedding_id == "node2":
found_node2 = True
assert metadata["workflow_id"] == "test_workflow"
assert metadata["node_id"] == "node2"
assert metadata["node_name"] == "Node 2"
assert found_node1, "Node1 metadata not found"
assert found_node2, "Node2 metadata not found"
# Vérifier que les vecteurs sont recherchables
query_vector = np.array([0.1, 0.2, 0.3], dtype=np.float32)
results = self.pipeline.faiss_manager.search_similar(query_vector, k=2)
assert len(results) == 2
# Le premier résultat devrait être node1 (vecteur identique)
assert results[0].embedding_id == "node1"
assert results[0].similarity > 0.99 # Quasi identique
if __name__ == "__main__":
pytest.main([__file__, "-v"])