""" Integration tests for warbler_cda.api.service FastAPI application. Tests API endpoints with TestClient to ensure proper request/response handling, error conditions, and endpoint integration. Targets 95% coverage for the service module. """ import pytest from fastapi.testclient import TestClient from unittest.mock import Mock, patch, MagicMock import json from datetime import datetime import asyncio class TestAPIServiceIntegration: """Test FastAPI service endpoints with TestClient.""" def setup_method(self): """Set up TestClient for each test.""" from warbler_cda.api.service import app self.client = TestClient(app) @patch('warbler_cda.api.service._init_api') def test_health_endpoint_basic(self, mock_init_api): """Test /health endpoint returns proper response structure.""" # Mock the API init to avoid actual initialization mock_api = Mock() mock_init_api.return_value = mock_api response = self.client.get("/health") assert response.status_code == 200 data = response.json() # Check required fields are present required_fields = [ "status", "uptime_seconds", "total_queries", "concurrent_queries", "max_concurrent_observed", "hybrid_queries", "errors" ] for field in required_fields: assert field in data, f"Missing required field: {field}" assert data["status"] == "healthy" assert isinstance(data["uptime_seconds"], (int, float)) assert data["uptime_seconds"] >= 0 def test_health_endpoint_response_model(self): """Test /health endpoint response matches Pydantic model.""" response = self.client.get("/health") assert response.status_code == 200 data = response.json() # Validate that all metrics are numbers assert isinstance(data["total_queries"], int) assert isinstance(data["concurrent_queries"], int) assert isinstance(data["max_concurrent_observed"], int) assert isinstance(data["hybrid_queries"], int) assert isinstance(data["errors"], int) @patch('warbler_cda.api.service._init_api') @patch('warbler_cda.api.service._auto_load_packs') def test_lifespan_events_executed(self, mock_auto_load, mock_init_api): """Test that lifespan events execute startup functions.""" from warbler_cda.api.service import lifespan from fastapi import FastAPI import asyncio # Create a new app instance for this test app = FastAPI(lifespan=lifespan) # Use lifespan context manager - this should trigger init functions async def test_lifespan(): async with lifespan(app): pass # Just enter and exit asyncio.run(test_lifespan()) # Verify startup functions were called mock_init_api.assert_called_once() mock_auto_load.assert_called_once() @patch('warbler_cda.api.service._init_api') def test_query_endpoint_validation_missing_query_id(self, mock_init_api): """Test /query endpoint validation for missing query_id.""" mock_api = Mock() mock_init_api.return_value = mock_api # Missing required query_id request_data = { "semantic_query": "test query" } response = self.client.post("/query", json=request_data) # Should get a 422 validation error assert response.status_code == 422 data = response.json() assert "detail" in data @patch('warbler_cda.api.service._init_api') def test_query_endpoint_validation_missing_semantic_query(self, mock_init_api): """Test /query endpoint accepts requests without semantic_query (per model defaults).""" mock_api = Mock() mock_init_api.return_value = mock_api # Valid request - defaults should handle missing semantic_query request_data = { "query_id": "test-123" } response = self.client.post("/query", json=request_data) # Should process the request (though may fail in retrieval) assert response.status_code in [200, 500] # Success or server error during processing @patch('warbler_cda.api.service._init_api') def test_query_endpoint_basic_semantic_query(self, mock_init_api): """Test /query endpoint with basic semantic query.""" # Mock API and retrieval result mock_api = Mock() mock_assembly = Mock() mock_assembly.results = [] mock_api.retrieve_context.return_value = mock_assembly mock_init_api.return_value = mock_api request_data = { "query_id": "test-semantic", "semantic_query": "test query text", "max_results": 5, "confidence_threshold": 0.7 } response = self.client.post("/query", json=request_data) assert response.status_code == 200 data = response.json() # Verify response structure required_fields = [ "query_id", "result_count", "results", "execution_time_ms", "timestamp", "narrative_analysis", "bob_status" ] for field in required_fields: assert field in data, f"Missing required field: {field}" assert data["query_id"] == "test-semantic" assert data["result_count"] == 0 # No results in mock assert isinstance(data["results"], list) assert data["results"] == [] @patch('warbler_cda.api.service._init_api') def test_query_endpoint_with_mock_results(self, mock_init_api): """Test /query endpoint with mock retrieval results.""" # Mock API and retrieval result with actual data mock_api = Mock() mock_result = Mock() mock_result.content_id = "doc-123" mock_result.result_id = "res-123" mock_result.content_type = "text" mock_result.relevance_score = 0.95 mock_result.semantic_similarity = 0.88 mock_result.fractalstat_resonance = 0.75 mock_result.content = "Test content" mock_result.temporal_distance = 0.1 mock_result.anchor_connections = [] mock_result.provenance_depth = 1 mock_result.conflict_flags = [] mock_result.metadata = {"pack": "test-pack"} mock_assembly = Mock() mock_assembly.results = [mock_result] mock_api.retrieve_context.return_value = mock_assembly mock_init_api.return_value = mock_api request_data = { "query_id": "test-with-results", "semantic_query": "find wisdom about resilience", "max_results": 10 } response = self.client.post("/query", json=request_data) assert response.status_code == 200 data = response.json() assert data["query_id"] == "test-with-results" assert data["result_count"] == 1 assert len(data["results"]) == 1 result = data["results"][0] assert result["id"] == "doc-123" assert result["result_id"] == "res-123" assert result["relevance_score"] == 0.95 assert result["semantic_similarity"] == 0.88 assert result["fractalstat_resonance"] == 0.75 @patch('warbler_cda.api.service._init_api') def test_query_endpoint_hybrid_fractalstat(self, mock_init_api): """Test /query endpoint with hybrid FractalStat scoring.""" # Mock API mock_api = Mock() mock_assembly = Mock() mock_assembly.results = [] mock_api.retrieve_context.return_value = mock_assembly mock_init_api.return_value = mock_api request_data = { "query_id": "test-hybrid", "semantic_query": "test fractal query", "fractalstat_hybrid": True, "weight_semantic": 0.7, "weight_fractalstat": 0.3, "fractalstat_address": { "realm": {"type": "retrieval_query", "label": "test"}, "adjacency": "semantic_proximity", "luminosity": 0.8 } } response = self.client.post("/query", json=request_data) assert response.status_code == 200 data = response.json() assert data["query_id"] == "test-hybrid" # Verify the query construction includes hybrid parameters call_args = mock_api.retrieve_context.call_args[0][0] # First arg to retrieve_context # Check that RetrievalQuery was constructed with hybrid settings assert call_args.fractalstat_hybrid is True assert call_args.weight_semantic == 0.7 assert call_args.weight_fractalstat == 0.3 @patch('warbler_cda.api.service._init_api') def test_query_endpoint_error_handling(self, mock_init_api): """Test /query endpoint error handling.""" # Mock API to raise an exception mock_api = Mock() mock_api.retrieve_context.side_effect = Exception("Retrieval failed") mock_init_api.return_value = mock_api request_data = { "query_id": "test-error", "semantic_query": "failing query" } response = self.client.post("/query", json=request_data) assert response.status_code == 500 data = response.json() assert "detail" in data def test_bulk_query_endpoint_validation(self): """Test /bulk_query endpoint input validation.""" # Test valid input structure (just validate the endpoint accepts proper format) request_data = { "queries": [ { "query_id": "bulk-1", "semantic_query": "query one" }, { "query_id": "bulk-2", "semantic_query": "query two" } ], "concurrency_level": 2 } # This will likely fail during execution due to mocking, but validates input structure response = self.client.post("/bulk_query", json=request_data) # Just check that it's a proper JSON response (not validation error) assert response.status_code != 422 # Not a validation error assert response.headers.get("content-type", "").startswith("application/json") def test_ingest_endpoint_validation(self): """Test /ingest endpoint input validation.""" # Missing documents array response = self.client.post("/ingest", json={}) assert response.status_code == 400 # Empty documents array response = self.client.post("/ingest", json={"documents": []}) assert response.status_code == 400 @patch('warbler_cda.api.service._init_api') def test_ingest_endpoint_success(self, mock_init_api): """Test /ingest endpoint successful document ingestion.""" mock_api = Mock() mock_api.add_document.return_value = True mock_api.get_context_store_size.return_value = 5 mock_init_api.return_value = mock_api request_data = { "documents": [ { "content_id": "doc-1", "content": "Test document content", "metadata": {"source": "test"} }, { "content_id": "doc-2", "content": "Another test document", "metadata": {"category": "example"} } ] } response = self.client.post("/ingest", json=request_data) assert response.status_code == 200 data = response.json() assert data["status"] == "success" assert data["ingested"] == 2 assert data["total_requested"] == 2 assert data["failed"] == 0 assert data["context_store_size"] == 5 @patch('warbler_cda.api.service._init_api') def test_ingest_endpoint_partial_failure(self, mock_init_api): """Test /ingest endpoint with some documents failing.""" mock_api = Mock() # First document succeeds, second fails mock_api.add_document.side_effect = [True, False] mock_api.get_context_store_size.return_value = 10 mock_init_api.return_value = mock_api request_data = { "documents": [ { "content_id": "doc-1", "content": "Good document" }, { "content_id": "doc-2", "content": "Duplicate document" # Mock will make this fail } ] } response = self.client.post("/ingest", json=request_data) assert response.status_code == 200 data = response.json() assert data["ingested"] == 1 assert data["failed"] == 1 assert "failed_documents" in data @patch('warbler_cda.api.service._init_api') def test_ingest_endpoint_missing_content_id(self, mock_init_api): """Test /ingest endpoint handles missing content_id.""" mock_api = Mock() mock_api.get_context_store_size.return_value = 8 mock_init_api.return_value = mock_api request_data = { "documents": [ { "content": "Document without ID", "metadata": {} } ] } response = self.client.post("/ingest", json=request_data) assert response.status_code == 200 data = response.json() assert data["ingested"] == 0 assert data["failed"] == 1 assert len(data["failed_documents"]) == 1 def test_metrics_endpoint(self): """Test /metrics endpoint returns current metrics.""" response = self.client.get("/metrics") assert response.status_code == 200 data = response.json() # Check expected metrics fields expected_fields = [ "timestamp", "total_queries", "concurrent_queries", "max_concurrent", "hybrid_queries", "errors", "start_time" ] for field in expected_fields: assert field in data, f"Missing metrics field: {field}" def test_metrics_reset_endpoint(self): """Test /metrics/reset endpoint resets counters.""" # First get current metrics initial_response = self.client.get("/metrics") initial_data = initial_response.json() # Reset metrics reset_response = self.client.post("/metrics/reset") assert reset_response.status_code == 200 data = reset_response.json() assert data["status"] == "metrics reset" # Get metrics after reset final_response = self.client.get("/metrics") final_data = final_response.json() # Check that metrics were reset to zero assert final_data["total_queries"] == 0 assert final_data["concurrent_queries"] == 0 assert final_data["hybrid_queries"] == 0 assert final_data["errors"] == 0 @patch('warbler_cda.api.service._analyze_narrative_coherence') def test_narrative_analysis_integration(self, mock_analyze): """Test narrative coherence analysis is called and results are returned.""" mock_analyze.return_value = { "coherence_score": 0.85, "narrative_threads": 2, "result_count": 3, "analysis": "Test analysis result" } # Make a query to trigger narrative analysis mock_api = Mock() mock_assembly = Mock() mock_assembly.results = [] with patch('warbler_cda.api.service._init_api', return_value=mock_api): with patch.object(mock_api, 'retrieve_context', return_value=mock_assembly): request_data = { "query_id": "narrative-test", "semantic_query": "analyze this" } response = self.client.post("/query", json=request_data) assert response.status_code == 200 data = response.json() assert "narrative_analysis" in data analysis = data["narrative_analysis"] assert analysis["coherence_score"] == 0.85 assert analysis["narrative_threads"] == 2 # Verify the analysis function was called mock_analyze.assert_called_once() @patch('warbler_cda.api.service._bob_skeptic_filter') def test_bob_skeptic_integration(self, mock_bob_filter): """Test Bob the Skeptic verification is integrated.""" mock_bob_filter.return_value = ("VERIFIED", {"test": "verification_log"}) # Make a query to trigger Bob verification mock_api = Mock() mock_assembly = Mock() mock_assembly.results = [] with patch('warbler_cda.api.service._init_api', return_value=mock_api): with patch.object(mock_api, 'retrieve_context', return_value=mock_assembly): request_data = { "query_id": "bob-test", "semantic_query": "skeptic test" } response = self.client.post("/query", json=request_data) assert response.status_code == 200 data = response.json() assert data["bob_status"] == "VERIFIED" assert data["bob_verification_log"] == {"test": "verification_log"} # Verify Bob filter was called mock_bob_filter.assert_called_once() class TestAPIServiceEdgeCases: """Test edge cases and error conditions.""" def setup_method(self): """Set up TestClient for each test.""" from warbler_cda.api.service import app self.client = TestClient(app) def test_health_endpoint_fields_are_numeric(self): """Test that all health endpoint fields are proper numeric types.""" response = self.client.get("/health") assert response.status_code == 200 data = response.json() # Ensure numeric fields are actually numeric (not strings) numeric_fields = [ "uptime_seconds", "total_queries", "concurrent_queries", "max_concurrent_observed", "hybrid_queries", "errors" ] for field in numeric_fields: value = data[field] assert isinstance(value, (int, float)), f"Field {field} should be numeric, got {type(value)}" def test_metrics_endpoint_timestamp_format(self): """Test that metrics endpoint returns proper timestamp.""" response = self.client.get("/metrics") assert response.status_code == 200 data = response.json() assert "timestamp" in data # Should be ISO format timestamp timestamp = data["timestamp"] try: datetime.fromisoformat(timestamp.replace('Z', '+00:00')) except ValueError: pytest.fail(f"Invalid timestamp format: {timestamp}") def test_bulk_query_empty_queries(self): """Test /bulk_query with empty queries array.""" request_data = { "queries": [], "concurrency_level": 1 } response = self.client.post("/bulk_query", json=request_data) assert response.status_code == 200 data = response.json() assert data["total_queries"] == 0 assert data["successful"] == 0 assert data["failed"] == 0 assert data["results"] == [] def test_query_endpoint_mode_enum_validation(self): """Test that query mode is properly validated and mapped.""" # Test different valid modes valid_modes = ["semantic_similarity", "temporal_sequence", "composite"] for mode in valid_modes: mock_api = Mock() mock_assembly = Mock() mock_assembly.results = [] with patch('warbler_cda.api.service._init_api', return_value=mock_api): with patch.object(mock_api, 'retrieve_context', return_value=mock_assembly): request_data = { "query_id": f"mode-test-{mode}", "mode": mode, "semantic_query": "test query" } response = self.client.post("/query", json=request_data) # Should not get validation error for mode assert response.status_code in [200, 500] # Success or processing error (not validation) def test_fractalstat_address_validation(self): """Test FractalStat address validation in requests.""" request_data = { "query_id": "address-test", "semantic_query": "test query", "fractalstat_hybrid": True, "fractalstat_address": { "realm": {"type": "retrieval_query"}, "adjacency": "semantic_proximity", "luminosity": 0.8 } } # Should accept valid FractalStat address response = self.client.post("/query", json=request_data) assert response.status_code in [200, 500] # Should not be validation error def test_concurrent_queries_metric_tracking(self): """Test that concurrent queries are properly tracked in metrics.""" # Get initial metrics initial_response = self.client.get("/metrics") initial_concurrent = initial_response.json()["concurrent_queries"] # This would require actual async testing with httpx/AsyncClient # For now, just verify the metric exists and is tracked assert isinstance(initial_concurrent, int) assert initial_concurrent >= 0 class TestAPIServiceLoadTesting: """Test API service under load conditions.""" def setup_method(self): """Set up TestClient for each test.""" from warbler_cda.api.service import app self.client = TestClient(app) def test_multiple_rapid_requests(self): """Test multiple rapid requests to various endpoints.""" endpoints = ["/health", "/metrics"] for _ in range(10): for endpoint in endpoints: response = self.client.get(endpoint) assert response.status_code == 200 def test_bulk_query_concurrency_limits(self): """Test bulk query handles concurrency limits properly.""" # Create multiple queries queries = [] for i in range(5): queries.append({ "query_id": f"load-test-{i}", "semantic_query": f"load test query {i}", "max_results": 3 }) request_data = { "queries": queries, "concurrency_level": 3 # Limit concurrency } response = self.client.post("/bulk_query", json=request_data) assert response.status_code == 200 data = response.json() assert data["total_queries"] == 5 # May have some failures due to mock setup, but structure should be correct # Performance and coverage targets class TestServiceCoverageGoals: """Tests specifically added to reach coverage targets.""" def setup_method(self): """Set up TestClient for each test.""" from warbler_cda.api.service import app self.client = TestClient(app) def test_query_result_structure_complete(self): """Test complete structure of QueryResult model.""" # This test ensures we hit response model serialization paths response = self.client.get("/health") # Simple endpoint to verify app works assert response.status_code == 200 # The query endpoint tests above should cover most of QueryResult serialization # This is just a placeholder to track coverage progress def test_metrics_reset_functionality(self): """Test metrics reset affects global state correctly.""" # First make some "requests" by calling health endpoint multiple times for _ in range(3): self.client.get("/health") # Reset should clear accumulated state reset_response = self.client.post("/metrics/reset") assert reset_response.status_code == 200 # Verify reset worked metrics_response = self.client.get("/metrics") metrics_data = metrics_response.json() # These should be back to initial values assert metrics_data["total_queries"] == 0 assert metrics_data["errors"] == 0