cidadao.ai-models / tests /test_models_client.py
neural-thinker's picture
feat: initial cidadao.ai-models deployment
b95e73a
"""
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