""" Tests unitaires pour FAISS Rebuild Propre Auteur : Dom, Alice Kiro - 22 décembre 2025 Tests pour les nouvelles fonctionnalités: - FAISSManager.clear() amélioré - FAISSManager.reindex() nouveau - WorkflowPipeline._extract_node_vector() multi-version """ import pytest import numpy as np import tempfile from pathlib import Path from unittest.mock import Mock, patch, MagicMock # Import conditionnel pour éviter les erreurs de dépendances try: from core.embedding.faiss_manager import FAISSManager FAISS_AVAILABLE = True except ImportError: FAISS_AVAILABLE = False FAISSManager = None try: from core.pipeline.workflow_pipeline import WorkflowPipeline PIPELINE_AVAILABLE = True except ImportError: PIPELINE_AVAILABLE = False WorkflowPipeline = None @pytest.mark.skipif(not FAISS_AVAILABLE, reason="FAISS not available") class TestFAISSManagerClear: """Tests pour la méthode clear() améliorée""" def test_faiss_clear_resets_state_flat(self): """Test que clear() reset complètement l'état pour index Flat""" manager = FAISSManager(dimensions=128, index_type="Flat") # Ajouter quelques embeddings vector1 = np.random.rand(128).astype(np.float32) vector2 = np.random.rand(128).astype(np.float32) manager.add_embedding("test1", vector1, {"meta": "data1"}) manager.add_embedding("test2", vector2, {"meta": "data2"}) # 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 # 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 def test_faiss_clear_resets_state_ivf(self): """Test que clear() reset complètement l'état IVF training""" manager = FAISSManager(dimensions=128, index_type="IVF") # Simuler des vecteurs d'entraînement for i in range(5): vector = np.random.rand(128).astype(np.float32) manager.training_vectors.append(vector) manager.next_id = 5 manager.metadata_store[0] = {"test": "data"} # 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 @pytest.mark.skipif(not FAISS_AVAILABLE, reason="FAISS not available") class TestFAISSManagerReindex: """Tests pour la méthode reindex() nouvelle""" def test_faiss_reindex_flat_removes_old_entries(self): """Test que reindex() supprime complètement les anciennes entrées (Flat)""" manager = FAISSManager(dimensions=128, index_type="Flat") # Ajouter des embeddings initiaux 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"}) manager.add_embedding("old2", old_vector2, {"type": "old"}) assert manager.index.ntotal == 2 # Préparer nouveaux items new_vector1 = np.random.rand(128).astype(np.float32) new_vector2 = np.random.rand(128).astype(np.float32) items = [ ("new1", new_vector1, {"type": "new"}), ("new2", new_vector2, {"type": "new"}) ] # 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" def test_faiss_reindex_ivf_trains_even_small(self): """Test que reindex() force training IVF même avec petit dataset""" manager = FAISSManager(dimensions=128, index_type="IVF") # Préparer petit dataset (< 100 vecteurs) items = [] for i in range(10): vector = np.random.rand(128).astype(np.float32) items.append((f"item_{i}", vector, {"index": i})) # Mock _train_ivf_index pour vérifier qu'il est appelé with patch.object(manager, '_train_ivf_index') as mock_train: count = manager.reindex(items, force_train_ivf=True) # Vérifier que training a été forcé assert count == 10 mock_train.assert_called_once() def test_faiss_reindex_handles_invalid_vectors(self): """Test que reindex() ignore les vecteurs invalides et continue""" 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 ] # 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 def test_faiss_reindex_returns_correct_count(self): """Test que reindex() retourne le bon nombre d'items traités""" manager = FAISSManager(dimensions=128, index_type="Flat") # Préparer items items = [] for i in range(15): vector = np.random.rand(128).astype(np.float32) items.append((f"item_{i}", vector, {"index": i})) # Reindex count = manager.reindex(items) # Vérifier count assert count == 15 assert manager.index.ntotal == 15 @pytest.mark.skipif(not PIPELINE_AVAILABLE, reason="Pipeline not available") class TestWorkflowPipelineExtractNodeVector: """Tests pour _extract_node_vector() multi-version""" def test_extract_node_vector_v1_list_format(self): """Test extraction vecteur format v1 (liste directe)""" pipeline = WorkflowPipeline() # Mock node avec template.embedding_prototype en liste node = Mock() template = Mock() template.embedding_prototype = [0.1, 0.2, 0.3, 0.4] node.template = template # Extraire vecteur vector = 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, [0.1, 0.2, 0.3, 0.4]) def test_extract_node_vector_v2_file_format(self): """Test extraction vecteur format v2 (fichier sur disque)""" pipeline = WorkflowPipeline() # Créer fichier temporaire avec vecteur with tempfile.NamedTemporaryFile(suffix='.npy', delete=False) as tmp: test_vector = np.array([0.5, 0.6, 0.7, 0.8], dtype=np.float32) np.save(tmp.name, test_vector) tmp_path = tmp.name try: # Mock node avec embedding.vector_id node = Mock() template = Mock() embedding = Mock() embedding.vector_id = tmp_path template.embedding = embedding template.embedding_prototype = None # Pas de liste directe node.template = template # Extraire vecteur vector = 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, [0.5, 0.6, 0.7, 0.8]) finally: # Nettoyer fichier temporaire Path(tmp_path).unlink(missing_ok=True) def test_extract_node_vector_v2_format(self): """Test extraction vecteur format v2 (template.embedding.vector_id)""" pipeline = WorkflowPipeline() # Créer fichier temporaire avec vecteur with tempfile.NamedTemporaryFile(suffix='.npy', delete=False) as tmp: test_vector = np.array([0.9, 1.0, 1.1, 1.2], dtype=np.float32) np.save(tmp.name, test_vector) tmp_path = tmp.name try: # Mock node avec template.embedding.vector_id (format v2) node = Mock() node.metadata = {} embedding = Mock() embedding.vector_id = tmp_path template = Mock() template.embedding = embedding template.embedding_prototype = None node.template = template # Extraire vecteur vector = 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, [0.9, 1.0, 1.1, 1.2]) finally: # Nettoyer fichier temporaire Path(tmp_path).unlink(missing_ok=True) def test_extract_node_vector_graceful_failure(self): """Test que _extract_node_vector() retourne None gracieusement""" pipeline = WorkflowPipeline() # Test avec node sans vecteur node = Mock() node.template = None node.metadata = {} vector = pipeline._extract_node_vector(node) assert vector is None # Test avec template mais pas de vecteur node2 = Mock() template = Mock() template.embedding_prototype = None template.embedding = None node2.template = template node2.metadata = {} vector2 = pipeline._extract_node_vector(node2) assert vector2 is None # Test avec fichier inexistant node3 = Mock() template3 = Mock() embedding3 = Mock() embedding3.vector_id = "/path/that/does/not/exist.npy" template3.embedding = embedding3 template3.embedding_prototype = None node3.template = template3 vector3 = pipeline._extract_node_vector(node3) assert vector3 is None @pytest.mark.skipif(not PIPELINE_AVAILABLE, reason="Pipeline not available") class TestWorkflowPipelineIndexWorkflowEmbeddings: """Tests pour _index_workflow_embeddings() amélioré""" def test_index_workflow_embeddings_completeness(self): """Test que tous les vecteurs valides sont extraits et indexés""" pipeline = WorkflowPipeline() # Mock workflow avec plusieurs nodes workflow = Mock() workflow.workflow_id = "test_workflow" # Node avec vecteur valide node1 = Mock() node1.node_id = "node1" node1.name = "Node 1" template1 = Mock() template1.embedding_prototype = [0.1, 0.2, 0.3] node1.template = template1 # Node avec vecteur valide node2 = Mock() node2.node_id = "node2" node2.name = "Node 2" template2 = Mock() template2.embedding_prototype = [0.4, 0.5, 0.6] node2.template = template2 # Node sans vecteur node3 = Mock() node3.node_id = "node3" node3.name = "Node 3" node3.template = None workflow.nodes = [node1, node2, node3] # Mock faiss_manager.reindex with patch.object(pipeline.faiss_manager, 'reindex') as mock_reindex: mock_reindex.return_value = 2 # Indexer pipeline._index_workflow_embeddings(workflow) # Vérifier appel reindex mock_reindex.assert_called_once() args, kwargs = mock_reindex.call_args items = args[0] # Vérifier items items_list = list(items) assert len(items_list) == 2 # Seulement les 2 nodes avec vecteurs # Vérifier premier item embedding_id1, vector1, metadata1 = items_list[0] assert embedding_id1 == "node1" assert np.allclose(vector1, [0.1, 0.2, 0.3]) assert metadata1["workflow_id"] == "test_workflow" assert metadata1["node_id"] == "node1" assert metadata1["node_name"] == "Node 1" # Vérifier force_train_ivf assert kwargs["force_train_ivf"] == True if __name__ == "__main__": pytest.main([__file__])