Spaces:
Sleeping
Sleeping
| """ | |
| Tests for Models Client (Backend Integration) | |
| Test suite for the models client with fallback functionality. | |
| """ | |
| import pytest | |
| import asyncio | |
| from unittest.mock import Mock, AsyncMock, patch | |
| import httpx | |
| import sys | |
| import os | |
| # Add backend path for testing | |
| backend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "..", "cidadao.ai-backend") | |
| sys.path.insert(0, backend_path) | |
| from src.tools.models_client import ModelsClient, ModelAPIStatus | |
| class TestModelsClient: | |
| """Test suite for ModelsClient.""" | |
| def client(self): | |
| """Create models client instance.""" | |
| return ModelsClient( | |
| base_url="http://localhost:8001", | |
| timeout=5.0, | |
| enable_fallback=True | |
| ) | |
| def sample_contracts(self): | |
| """Sample contract data.""" | |
| return [ | |
| { | |
| "id": "CT001", | |
| "description": "Test contract", | |
| "value": 100000.0, | |
| "supplier": "Company A", | |
| "date": "2024-01-01", | |
| "organ": "Ministry X" | |
| } | |
| ] | |
| def test_client_initialization(self): | |
| """Test client initialization with different configs.""" | |
| # Default initialization | |
| client = ModelsClient() | |
| assert client.base_url == "http://localhost:8001" | |
| assert client.timeout == 30.0 | |
| assert client.enable_fallback is True | |
| assert client.status == ModelAPIStatus.ONLINE | |
| # Custom initialization | |
| client = ModelsClient( | |
| base_url="http://models:8080", | |
| timeout=10.0, | |
| enable_fallback=False | |
| ) | |
| assert client.base_url == "http://models:8080" | |
| assert client.timeout == 10.0 | |
| assert client.enable_fallback is False | |
| async def test_health_check_success(self, client): | |
| """Test successful health check.""" | |
| # Mock successful response | |
| with patch.object(client.client, 'get') as mock_get: | |
| mock_response = Mock() | |
| mock_response.json.return_value = { | |
| "status": "healthy", | |
| "models_loaded": True | |
| } | |
| mock_response.raise_for_status = Mock() | |
| mock_get.return_value = mock_response | |
| result = await client.health_check() | |
| assert result["status"] == "healthy" | |
| assert client.status == ModelAPIStatus.ONLINE | |
| assert client._failure_count == 0 | |
| async def test_health_check_failure(self, client): | |
| """Test health check failure handling.""" | |
| # Mock failed response | |
| with patch.object(client.client, 'get') as mock_get: | |
| mock_get.side_effect = httpx.RequestError("Connection failed") | |
| result = await client.health_check() | |
| assert result["status"] == "unhealthy" | |
| assert "error" in result | |
| assert result["fallback_available"] is True | |
| assert client._failure_count == 1 | |
| async def test_detect_anomalies_api_success(self, client, sample_contracts): | |
| """Test successful anomaly detection via API.""" | |
| expected_response = { | |
| "anomalies": [], | |
| "total_analyzed": 1, | |
| "anomalies_found": 0, | |
| "confidence_score": 0.95, | |
| "model_version": "1.0.0" | |
| } | |
| with patch.object(client.client, 'post') as mock_post: | |
| mock_response = Mock() | |
| mock_response.json.return_value = expected_response | |
| mock_response.raise_for_status = Mock() | |
| mock_post.return_value = mock_response | |
| result = await client.detect_anomalies(sample_contracts) | |
| assert result == expected_response | |
| assert client.status == ModelAPIStatus.ONLINE | |
| # Verify request was made correctly | |
| mock_post.assert_called_once() | |
| call_args = mock_post.call_args | |
| assert call_args[0][0] == "/v1/detect-anomalies" | |
| assert "contracts" in call_args[1]["json"] | |
| async def test_detect_anomalies_with_fallback(self, client, sample_contracts): | |
| """Test anomaly detection with fallback to local ML.""" | |
| # Mock API failure | |
| with patch.object(client.client, 'post') as mock_post: | |
| mock_post.side_effect = httpx.RequestError("API unavailable") | |
| # Mock local ML | |
| with patch.object(client, '_local_anomaly_detection') as mock_local: | |
| mock_local.return_value = { | |
| "anomalies": [], | |
| "total_analyzed": 1, | |
| "anomalies_found": 0, | |
| "confidence_score": 0.85, | |
| "model_version": "local-1.0.0", | |
| "source": "local_fallback" | |
| } | |
| result = await client.detect_anomalies(sample_contracts) | |
| assert result["source"] == "local_fallback" | |
| assert result["confidence_score"] == 0.85 | |
| mock_local.assert_called_once_with(sample_contracts, 0.7) | |
| async def test_detect_anomalies_no_fallback_error(self, client, sample_contracts): | |
| """Test anomaly detection error when fallback is disabled.""" | |
| client.enable_fallback = False | |
| with patch.object(client.client, 'post') as mock_post: | |
| mock_post.side_effect = httpx.RequestError("API unavailable") | |
| with pytest.raises(httpx.RequestError): | |
| await client.detect_anomalies(sample_contracts) | |
| async def test_analyze_patterns_success(self, client): | |
| """Test successful pattern analysis.""" | |
| test_data = {"contracts": [{"value": 100000}]} | |
| expected_response = { | |
| "patterns": [{"type": "temporal"}], | |
| "pattern_count": 1, | |
| "confidence": 0.88, | |
| "insights": ["Pattern detected"] | |
| } | |
| with patch.object(client.client, 'post') as mock_post: | |
| mock_response = Mock() | |
| mock_response.json.return_value = expected_response | |
| mock_response.raise_for_status = Mock() | |
| mock_post.return_value = mock_response | |
| result = await client.analyze_patterns(test_data) | |
| assert result == expected_response | |
| assert client.status == ModelAPIStatus.ONLINE | |
| async def test_analyze_spectral_success(self, client): | |
| """Test successful spectral analysis.""" | |
| time_series = [100, 200, 150, 300, 250] | |
| expected_response = { | |
| "frequencies": [0.1, 0.5], | |
| "amplitudes": [10.0, 20.0], | |
| "dominant_frequency": 0.5, | |
| "periodic_patterns": [] | |
| } | |
| with patch.object(client.client, 'post') as mock_post: | |
| mock_response = Mock() | |
| mock_response.json.return_value = expected_response | |
| mock_response.raise_for_status = Mock() | |
| mock_post.return_value = mock_response | |
| result = await client.analyze_spectral(time_series) | |
| assert result == expected_response | |
| assert result["dominant_frequency"] == 0.5 | |
| def test_circuit_breaker_functionality(self, client): | |
| """Test circuit breaker marks API as offline after failures.""" | |
| # Initial state | |
| assert client.status == ModelAPIStatus.ONLINE | |
| assert client._failure_count == 0 | |
| # First failure | |
| client._handle_failure() | |
| assert client.status == ModelAPIStatus.DEGRADED | |
| assert client._failure_count == 1 | |
| # Second failure | |
| client._handle_failure() | |
| assert client.status == ModelAPIStatus.DEGRADED | |
| assert client._failure_count == 2 | |
| # Third failure - circuit opens | |
| client._handle_failure() | |
| assert client.status == ModelAPIStatus.OFFLINE | |
| assert client._failure_count == 3 | |
| # Reset on success | |
| client._reset_failure_count() | |
| assert client.status == ModelAPIStatus.ONLINE | |
| assert client._failure_count == 0 | |
| async def test_context_manager(self): | |
| """Test client as async context manager.""" | |
| async with ModelsClient() as client: | |
| assert isinstance(client, ModelsClient) | |
| assert client.status == ModelAPIStatus.ONLINE | |
| # Client should be closed after context | |
| with pytest.raises(RuntimeError): | |
| await client.health_check() | |
| async def test_local_fallback_caching(self, client, sample_contracts): | |
| """Test local models are cached for performance.""" | |
| # Force use of local fallback | |
| client.status = ModelAPIStatus.OFFLINE | |
| # First call - creates model | |
| result1 = await client._local_anomaly_detection(sample_contracts, 0.7) | |
| assert "anomaly_detector" in client._local_models | |
| # Second call - reuses model | |
| initial_model = client._local_models["anomaly_detector"] | |
| result2 = await client._local_anomaly_detection(sample_contracts, 0.7) | |
| # Should be same model instance | |
| assert client._local_models["anomaly_detector"] is initial_model | |
| def test_singleton_client(self): | |
| """Test singleton client instance.""" | |
| from src.tools.models_client import get_models_client | |
| client1 = get_models_client() | |
| client2 = get_models_client() | |
| assert client1 is client2 # Same instance |