File size: 9,948 Bytes
b95e73a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
"""
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."""
    
    @pytest.fixture
    def client(self):
        """Create models client instance."""
        return ModelsClient(
            base_url="http://localhost:8001",
            timeout=5.0,
            enable_fallback=True
        )
    
    @pytest.fixture
    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
    
    @pytest.mark.asyncio
    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
    
    @pytest.mark.asyncio
    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
    
    @pytest.mark.asyncio
    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"]
    
    @pytest.mark.asyncio
    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)
    
    @pytest.mark.asyncio
    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)
    
    @pytest.mark.asyncio
    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
    
    @pytest.mark.asyncio
    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
    
    @pytest.mark.asyncio
    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()
    
    @pytest.mark.asyncio
    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