"""Tests for Core-AI client.""" import pytest from unittest.mock import patch, AsyncMock, MagicMock import httpx from src.clients.ai_client import CoreAIClient, get_ai_client class TestCoreAIClientInit: """Test CoreAIClient initialization.""" @patch("src.clients.ai_client.settings") def test_uses_settings_defaults(self, mock_settings): """Client should use settings for defaults.""" mock_settings.core_ai_base_url = "http://core-ai:8086" client = CoreAIClient() assert client.base_url == "http://core-ai:8086" assert client.timeout == 10 def test_accepts_custom_url(self): """Client should accept custom URL.""" client = CoreAIClient(base_url="http://custom:9000") assert client.base_url == "http://custom:9000" def test_accepts_custom_timeout(self): """Client should accept custom timeout.""" client = CoreAIClient(base_url="http://test:8086", timeout=30) assert client.timeout == 30 def test_strips_trailing_slash_from_url(self): """Client should strip trailing slash from URL.""" client = CoreAIClient(base_url="http://core-ai:8086/") assert client.base_url == "http://core-ai:8086" def test_creates_http_client(self): """Client should create httpx AsyncClient.""" client = CoreAIClient(base_url="http://test:8086") assert client.client is not None class TestCoreAIClientClose: """Test client close functionality.""" @pytest.mark.asyncio async def test_close_closes_client(self): """close should close the HTTP client.""" client = CoreAIClient(base_url="http://test:8086") with patch.object(client.client, "aclose", new_callable=AsyncMock) as mock_close: await client.close() mock_close.assert_called_once() class TestCoreAIClientContextManager: """Test async context manager.""" @pytest.mark.asyncio async def test_context_manager_enters(self): """Context manager should return client on enter.""" client = CoreAIClient(base_url="http://test:8086") with patch.object(client.client, "aclose", new_callable=AsyncMock): async with client as ctx: assert ctx is client @pytest.mark.asyncio async def test_context_manager_closes_on_exit(self): """Context manager should close client on exit.""" client = CoreAIClient(base_url="http://test:8086") with patch.object(client, "close", new_callable=AsyncMock) as mock_close: async with client: pass mock_close.assert_called_once() class TestCoreAIClientHealthCheck: """Test health check functionality.""" @pytest.mark.asyncio async def test_health_check_returns_true_on_200(self): """Health check should return True when service responds 200.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 200 with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await client.health_check() assert result is True @pytest.mark.asyncio async def test_health_check_returns_false_on_error(self): """Health check should return False on connection error.""" client = CoreAIClient(base_url="http://test:8086") with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.side_effect = Exception("Connection refused") result = await client.health_check() assert result is False @pytest.mark.asyncio async def test_health_check_returns_false_on_non_200(self): """Health check should return False on non-200 status.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 500 with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await client.health_check() assert result is False class TestCoreAIClientGetMetrics: """Test get metrics functionality.""" @pytest.mark.asyncio async def test_get_metrics_returns_dict(self): """get_metrics should return metrics dict.""" client = CoreAIClient(base_url="http://test:8086") metrics_data = { "uptime_seconds": 3600, "agent": {"total_requests": 100}, "tools": {"total_calls": 250} } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = metrics_data mock_response.raise_for_status = MagicMock() with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await client.get_metrics() assert result == metrics_data assert result["uptime_seconds"] == 3600 @pytest.mark.asyncio async def test_get_metrics_raises_on_http_error(self): """get_metrics should raise on HTTP error.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 500 mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( "Server Error", request=MagicMock(), response=mock_response ) with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response with pytest.raises(httpx.HTTPStatusError): await client.get_metrics() class TestCoreAIClientGetRecentErrors: """Test get recent errors functionality.""" @pytest.mark.asyncio async def test_get_recent_errors_returns_list(self): """get_recent_errors should return list of errors.""" client = CoreAIClient(base_url="http://test:8086") errors_data = { "errors": [ {"timestamp": "2025-12-03T19:45:12Z", "error": "Timeout"}, {"timestamp": "2025-12-03T19:46:00Z", "error": "Connection refused"} ] } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = errors_data mock_response.raise_for_status = MagicMock() with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await client.get_recent_errors() assert len(result) == 2 assert result[0]["error"] == "Timeout" @pytest.mark.asyncio async def test_get_recent_errors_passes_limit(self): """get_recent_errors should pass limit parameter.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"errors": []} mock_response.raise_for_status = MagicMock() with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response await client.get_recent_errors(limit=5) call_args = mock_get.call_args assert call_args[1]["params"]["limit"] == 5 class TestCoreAIClientGetToolFailures: """Test get tool failures functionality.""" @pytest.mark.asyncio async def test_get_tool_failures_returns_list(self): """get_tool_failures should return list of failures.""" client = CoreAIClient(base_url="http://test:8086") failures_data = { "failures": [ {"tool_name": "list_containers", "error": "Connection refused"} ] } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = failures_data mock_response.raise_for_status = MagicMock() with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response result = await client.get_tool_failures() assert len(result) == 1 assert result[0]["tool_name"] == "list_containers" @pytest.mark.asyncio async def test_get_tool_failures_passes_limit(self): """get_tool_failures should pass limit parameter.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"failures": []} mock_response.raise_for_status = MagicMock() with patch.object(client.client, "get", new_callable=AsyncMock) as mock_get: mock_get.return_value = mock_response await client.get_tool_failures(limit=10) call_args = mock_get.call_args assert call_args[1]["params"]["limit"] == 10 class TestCoreAIClientResetMetrics: """Test reset metrics functionality.""" @pytest.mark.asyncio async def test_reset_metrics_returns_true_on_success(self): """reset_metrics should return True on success.""" client = CoreAIClient(base_url="http://test:8086") mock_response = MagicMock() mock_response.status_code = 200 mock_response.raise_for_status = MagicMock() with patch.object(client.client, "post", new_callable=AsyncMock) as mock_post: mock_post.return_value = mock_response result = await client.reset_metrics() assert result is True @pytest.mark.asyncio async def test_reset_metrics_raises_on_error(self): """reset_metrics should raise on error.""" client = CoreAIClient(base_url="http://test:8086") with patch.object(client.client, "post", new_callable=AsyncMock) as mock_post: mock_post.side_effect = Exception("Connection refused") with pytest.raises(Exception): await client.reset_metrics() class TestCoreAIClientSingleton: """Test singleton pattern.""" def test_get_ai_client_returns_same_instance(self): """get_ai_client should return singleton.""" import src.clients.ai_client as module module._ai_client = None client1 = get_ai_client() client2 = get_ai_client() assert client1 is client2