""" Tests for maintenance router and cleanup functionality. Tests cleanup of: - Orphan vector chunks - Orphan entities in graph - Stale document nodes """ import pytest from unittest.mock import AsyncMock, MagicMock from src.routers.maintenance import ( cleanup_vectors, cleanup_graph, cleanup_all, maintenance_health, reindex_page, CleanupResult, VectorCleanupResponse, GraphCleanupResponse, HealthCheckResponse ) class TestCleanupResult: """Test CleanupResult model.""" def test_cleanup_result_defaults(self): """Test CleanupResult with default values.""" result = CleanupResult(duration_ms=100.0) assert result.orphans_found == 0 assert result.orphans_purged == 0 assert result.duration_ms == 100.0 def test_cleanup_result_with_values(self): """Test CleanupResult with actual values.""" result = CleanupResult( orphans_found=10, orphans_purged=8, duration_ms=250.5 ) assert result.orphans_found == 10 assert result.orphans_purged == 8 assert result.duration_ms == 250.5 class TestVectorCleanupResponse: """Test VectorCleanupResponse model.""" def test_vector_cleanup_response(self): """Test VectorCleanupResponse structure.""" response = VectorCleanupResponse( success=True, wiki_chunks=CleanupResult(orphans_found=5, orphans_purged=5, duration_ms=50), document_chunks=CleanupResult(orphans_found=3, orphans_purged=3, duration_ms=50), chunks_without_graph=CleanupResult(orphans_found=2, orphans_purged=2, duration_ms=50), total_chunks_scanned=100, total_orphans_purged=10, duration_ms=100 ) assert response.success is True assert response.wiki_chunks.orphans_found == 5 assert response.document_chunks.orphans_found == 3 assert response.chunks_without_graph.orphans_found == 2 assert response.total_orphans_purged == 10 class TestGraphCleanupResponse: """Test GraphCleanupResponse model.""" def test_graph_cleanup_response(self): """Test GraphCleanupResponse structure.""" response = GraphCleanupResponse( success=True, orphan_entities=CleanupResult(orphans_found=10, orphans_purged=10, duration_ms=25), stale_wiki_documents=CleanupResult(orphans_found=2, orphans_purged=2, duration_ms=25), stale_store_documents=CleanupResult(orphans_found=0, orphans_purged=0, duration_ms=25), docs_without_vectors=CleanupResult(orphans_found=1, orphans_purged=1, duration_ms=25), broken_relationships_cleaned=5, duration_ms=100 ) assert response.success is True assert response.orphan_entities.orphans_found == 10 assert response.docs_without_vectors.orphans_found == 1 assert response.broken_relationships_cleaned == 5 class TestHealthCheckResponse: """Test HealthCheckResponse model.""" def test_health_check_healthy(self): """Test healthy status.""" response = HealthCheckResponse( status="healthy", orphan_vector_count=0, orphan_entity_count=0, stale_document_count=0 ) assert response.status == "healthy" assert response.recommendations == [] def test_health_check_degraded(self): """Test degraded status with recommendations.""" response = HealthCheckResponse( status="degraded", orphan_vector_count=15, orphan_entity_count=3, stale_document_count=0, recommendations=[ "Found 15 orphan vector chunks. Consider running POST /maintenance/cleanup/vectors" ] ) assert response.status == "degraded" assert len(response.recommendations) == 1 @pytest.mark.asyncio class TestVectorCleanup: """Test vector cleanup endpoint.""" async def test_cleanup_vectors_no_orphans(self): """Test cleanup when no orphans exist.""" # Mock services vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": "c1", "page_id": 1, "doc_type": "wiki"} ] # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.get_all_document_references.return_value = [ {"page_id": 1, "doc_type": "wiki", "title": "Test"} ] wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [{"id": 1, "path": "test"}] # Call cleanup result = await cleanup_vectors( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.wiki_chunks.orphans_found == 0 assert result.chunks_without_graph.orphans_found == 0 assert result.total_orphans_purged == 0 async def test_cleanup_vectors_with_orphans(self): """Test cleanup when orphans exist.""" # Mock services vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": "c1", "page_id": 1, "doc_type": "wiki"}, {"chunk_id": "c2", "page_id": 999, "doc_type": "wiki"}, # Orphan {"chunk_id": "c3", "page_id": 999, "doc_type": "wiki"}, # Orphan ] vector_service.purge_chunks_by_ids.return_value = 2 # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.get_all_document_references.return_value = [ {"page_id": 1, "doc_type": "wiki", "title": "Test"} ] wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [{"id": 1, "path": "test"}] # Call cleanup result = await cleanup_vectors( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.wiki_chunks.orphans_found == 2 assert result.wiki_chunks.orphans_purged == 2 assert result.total_orphans_purged == 2 async def test_cleanup_vectors_dry_run(self): """Test cleanup dry run doesn't purge.""" # Mock services vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": "c1", "page_id": 999, "doc_type": "wiki"}, # Orphan ] # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.get_all_document_references.return_value = [] wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [] # Call cleanup in dry run mode result = await cleanup_vectors( user="testuser", dry_run=True, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.wiki_chunks.orphans_found == 1 assert result.wiki_chunks.orphans_purged == 0 # Not purged due to dry run vector_service.purge_chunks_by_ids.assert_not_called() @pytest.mark.asyncio class TestGraphCleanup: """Test graph cleanup endpoint.""" async def test_cleanup_graph_no_orphans(self): """Test cleanup when no orphans exist.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": "c1", "page_id": 1, "doc_type": "wiki"} ] graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [] graph_service.get_all_document_references.return_value = [ {"page_id": 1, "doc_type": "wiki", "title": "Test"} ] graph_service.find_documents_without_vectors.return_value = [] graph_service.cleanup_broken_relationships.return_value = 0 wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [{"id": 1, "path": "test"}] result = await cleanup_graph( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.orphan_entities.orphans_found == 0 assert result.stale_wiki_documents.orphans_found == 0 assert result.docs_without_vectors.orphans_found == 0 async def test_cleanup_graph_with_orphan_entities(self): """Test cleanup of orphan entities.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [] graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [ {"id": "e1", "name": "Orphan1", "type": "Person"}, {"id": "e2", "name": "Orphan2", "type": "Technology"}, ] graph_service.purge_orphan_entities.return_value = 2 graph_service.get_all_document_references.return_value = [] graph_service.find_documents_without_vectors.return_value = [] graph_service.cleanup_broken_relationships.return_value = 0 wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [] result = await cleanup_graph( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.orphan_entities.orphans_found == 2 assert result.orphan_entities.orphans_purged == 2 async def test_cleanup_graph_with_stale_documents(self): """Test cleanup of stale document nodes.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": "c1", "page_id": 1, "doc_type": "wiki"} ] graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [] graph_service.get_all_document_references.return_value = [ {"page_id": 1, "doc_type": "wiki", "title": "Exists"}, {"page_id": 999, "doc_type": "wiki", "title": "Deleted"}, # Stale ] graph_service.find_documents_without_vectors.return_value = [] graph_service.purge_stale_documents_by_ids.return_value = 1 graph_service.cleanup_broken_relationships.return_value = 0 wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [{"id": 1, "path": "test"}] result = await cleanup_graph( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.stale_wiki_documents.orphans_found == 1 assert result.stale_wiki_documents.orphans_purged == 1 @pytest.mark.asyncio class TestFullCleanup: """Test full cleanup endpoint.""" async def test_full_cleanup(self): """Test full cleanup runs both vector and graph cleanup.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [] # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [] graph_service.get_all_document_references.return_value = [] graph_service.find_documents_without_vectors.return_value = [] graph_service.cleanup_broken_relationships.return_value = 0 wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [] result = await cleanup_all( user="testuser", dry_run=False, vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.success is True assert result.vector_cleanup.success is True assert result.graph_cleanup.success is True @pytest.mark.asyncio class TestMaintenanceHealth: """Test maintenance health endpoint.""" async def test_health_healthy(self): """Test healthy status when no orphans.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [] # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [] graph_service.get_all_document_references.return_value = [] graph_service.find_documents_without_vectors.return_value = [] wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [] result = await maintenance_health( user="testuser", vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.status == "healthy" assert result.orphan_vector_count == 0 assert result.orphan_entity_count == 0 assert result.vectors_without_graph == 0 assert result.docs_without_vectors == 0 async def test_health_degraded(self): """Test degraded status with orphans.""" vector_service = AsyncMock() vector_service.get_all_chunk_references.return_value = [ {"chunk_id": f"c{i}", "page_id": 999, "doc_type": "wiki"} for i in range(15) ] # find_chunks_without_graph_nodes is not async vector_service.find_chunks_without_graph_nodes = MagicMock(return_value=[]) graph_service = AsyncMock() graph_service.find_orphan_entities.return_value = [ {"id": f"e{i}", "name": f"Entity{i}", "type": "Entity"} for i in range(3) ] graph_service.get_all_document_references.return_value = [] graph_service.find_documents_without_vectors.return_value = [] wiki_client = AsyncMock() wiki_client.list_all_pages.return_value = [] result = await maintenance_health( user="testuser", vector_service=vector_service, graph_service=graph_service, wiki_client=wiki_client, api_key="test" ) assert result.status == "degraded" assert result.orphan_vector_count == 15 assert result.orphan_entity_count == 3 assert len(result.recommendations) >= 1 @pytest.mark.asyncio class TestReindexPage: """Test reindex page endpoint.""" async def test_reindex_success(self): """Test successful page reindex.""" vector_service = AsyncMock() vector_service.delete_page_chunks.return_value = 5 vector_service.update_from_page.return_value = MagicMock( success=True, chunks_created=6, error_message=None ) graph_service = AsyncMock() graph_service.delete_page.return_value = 1 graph_service.update_from_page.return_value = MagicMock( success=True, error_message=None ) result = await reindex_page( page_id=123, user="testuser", vector_service=vector_service, graph_service=graph_service, api_key="test" ) assert result.success is True assert result.page_id == 123 assert result.vectors_deleted == 5 assert result.vectors_created == 6 assert result.graph_updated is True async def test_reindex_failure(self): """Test reindex with failure.""" vector_service = AsyncMock() vector_service.delete_page_chunks.return_value = 0 vector_service.update_from_page.return_value = MagicMock( success=False, chunks_created=0, error_message="Page not found" ) graph_service = AsyncMock() graph_service.delete_page.return_value = 0 graph_service.update_from_page.return_value = MagicMock( success=False, error_message="Page not found" ) result = await reindex_page( page_id=999, user="testuser", vector_service=vector_service, graph_service=graph_service, api_key="test" ) assert result.success is False assert result.error == "Page not found"