"""
Tests for the simplified centralized Google AI and Instructor client management.
This module contains unit tests for the functional client module using the new
Google GenAI SDK with instructor's from_provider approach.
"""
import os
from unittest.mock import MagicMock, patch
import pytest # type: ignore
from elrond_mcp.client import (
DEFAULT_CRITIQUE_MODEL,
DEFAULT_SYNTHESIS_MODEL,
configure,
get_critique_client,
get_synthesis_client,
is_configured,
reset,
)
class TestConfiguration:
"""Test configuration functionality."""
def setup_method(self):
"""Reset module state before each test."""
reset()
def teardown_method(self):
"""Clean up after each test."""
reset()
def test_configure_with_api_key(self):
"""Test explicit configuration with API key."""
api_key = "test-api-key-12345"
configure(api_key)
# Should set environment variable for instructor
assert os.environ.get("GEMINI_API_KEY") == api_key
assert is_configured()
def test_configure_empty_key_fails(self):
"""Test that empty API key fails validation."""
with pytest.raises(ValueError) as exc_info:
configure("")
assert "API key cannot be empty" in str(exc_info.value)
def test_configure_none_key_fails(self):
"""Test that None API key fails validation."""
with pytest.raises(ValueError) as exc_info:
configure(None) # type: ignore
assert "API key cannot be empty" in str(exc_info.value)
def test_auto_configure_from_gemini_api_key(self):
"""Test auto-configuration from GEMINI_API_KEY environment variable."""
api_key = "env-gemini-key"
with patch.dict(os.environ, {"GEMINI_API_KEY": api_key}, clear=True):
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
client = get_critique_client()
assert client == mock_client
assert is_configured()
mock_instructor.assert_called_once_with(
f"google/{DEFAULT_CRITIQUE_MODEL}", async_client=True
)
def test_auto_configure_from_google_api_key(self):
"""Test auto-configuration from GOOGLE_API_KEY environment variable."""
api_key = "env-google-key"
with patch.dict(os.environ, {"GOOGLE_API_KEY": api_key}, clear=True):
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
client = get_critique_client()
assert client == mock_client
assert is_configured()
def test_auto_configure_no_api_key_fails(self):
"""Test that auto-configuration fails when no API key is available."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError) as exc_info:
get_critique_client()
assert "Google AI API key is required" in str(exc_info.value)
def test_is_configured_states(self):
"""Test is_configured returns correct state."""
# Initially not configured
assert not is_configured()
# After explicit configuration
configure("test-key")
assert is_configured()
# After reset
reset()
assert not is_configured()
# With environment variable
with patch.dict(os.environ, {"GEMINI_API_KEY": "env-key"}):
assert is_configured()
class TestClientCreation:
"""Test client creation and caching functionality."""
def setup_method(self):
"""Reset module state before each test."""
reset()
def teardown_method(self):
"""Clean up after each test."""
reset()
def test_get_critique_client_default_model(self):
"""Test getting critique client with default model."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
configure(api_key)
client = get_critique_client()
mock_instructor.assert_called_once_with(
f"google/{DEFAULT_CRITIQUE_MODEL}", async_client=True
)
assert client == mock_client
def test_get_critique_client_custom_model(self):
"""Test getting critique client with custom model."""
api_key = "test-key"
custom_model = "gemini-1.5-pro"
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
configure(api_key)
client = get_critique_client(custom_model)
mock_instructor.assert_called_once_with(
f"google/{custom_model}", async_client=True
)
assert client == mock_client
def test_get_synthesis_client_default_model(self):
"""Test getting synthesis client with default model."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
configure(api_key)
client = get_synthesis_client()
mock_instructor.assert_called_once_with(
f"google/{DEFAULT_SYNTHESIS_MODEL}", async_client=True
)
assert client == mock_client
def test_get_synthesis_client_custom_model(self):
"""Test getting synthesis client with custom model."""
api_key = "test-key"
custom_model = "gemini-1.5-flash"
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
configure(api_key)
client = get_synthesis_client(custom_model)
mock_instructor.assert_called_once_with(
f"google/{custom_model}", async_client=True
)
assert client == mock_client
def test_client_caching(self):
"""Test that clients are cached and reused."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
configure(api_key)
# First call should create client
client1 = get_critique_client()
# Second call should return cached client
client2 = get_critique_client()
assert client1 is client2
# Should only call instructor once
assert mock_instructor.call_count == 1
def test_different_clients_are_separate(self):
"""Test that critique and synthesis clients are separate instances."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_critique_client = MagicMock()
mock_synthesis_client = MagicMock()
mock_instructor.side_effect = [mock_critique_client, mock_synthesis_client]
configure(api_key)
critique_client = get_critique_client()
synthesis_client = get_synthesis_client()
assert critique_client is not synthesis_client
assert critique_client == mock_critique_client
assert synthesis_client == mock_synthesis_client
assert mock_instructor.call_count == 2
def test_client_creation_failure(self):
"""Test client creation failure handling."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_instructor.side_effect = Exception("Provider creation failed")
configure(api_key)
with pytest.raises(Exception) as exc_info:
get_critique_client()
assert "Provider creation failed" in str(exc_info.value)
class TestReset:
"""Test reset functionality."""
def setup_method(self):
"""Reset module state before each test."""
reset()
def teardown_method(self):
"""Clean up after each test."""
reset()
def test_reset_clears_cached_clients(self):
"""Test that reset clears cached clients."""
api_key = "test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_client1 = MagicMock()
mock_client2 = MagicMock()
mock_instructor.side_effect = [mock_client1, mock_client2]
configure(api_key)
# Get initial client
client1 = get_critique_client()
assert client1 == mock_client1
# Reset and configure again
reset()
configure(api_key)
client2 = get_critique_client()
# Should be a new client instance
assert client2 == mock_client2
assert client1 is not client2
def test_reset_clears_environment_variable(self):
"""Test that reset clears the GEMINI_API_KEY environment variable."""
api_key = "test-key"
configure(api_key)
assert os.environ.get("GEMINI_API_KEY") == api_key
reset()
assert "GEMINI_API_KEY" not in os.environ
def test_reset_after_auto_configure(self):
"""Test reset after auto-configuration from environment."""
api_key = "env-key"
with patch.dict(os.environ, {"GEMINI_API_KEY": api_key}):
with patch("instructor.from_provider"):
get_critique_client() # Triggers auto-config
assert is_configured()
reset()
# Should not be configured anymore
# (even though env var still exists, internal state is cleared)
with patch.dict(os.environ, {}, clear=True):
assert not is_configured()
class TestConstants:
"""Test module constants."""
def test_default_models(self):
"""Test that default model constants are properly set."""
assert DEFAULT_CRITIQUE_MODEL == "gemini-2.5-flash"
assert DEFAULT_SYNTHESIS_MODEL == "gemini-2.5-pro"
class TestIntegration:
"""Test integration scenarios."""
def setup_method(self):
"""Reset module state before each test."""
reset()
def teardown_method(self):
"""Clean up after each test."""
reset()
def test_full_workflow_explicit_config(self):
"""Test complete workflow with explicit configuration."""
api_key = "integration-test-key"
with patch("instructor.from_provider") as mock_instructor:
mock_critique_client = MagicMock()
mock_synthesis_client = MagicMock()
mock_instructor.side_effect = [mock_critique_client, mock_synthesis_client]
# Explicit configuration
configure(api_key)
assert is_configured()
# Get clients
critique_client = get_critique_client()
synthesis_client = get_synthesis_client()
# Verify clients are distinct
assert critique_client is not synthesis_client
assert critique_client == mock_critique_client
assert synthesis_client == mock_synthesis_client
# Verify correct instructor calls
expected_calls = [
(f"google/{DEFAULT_CRITIQUE_MODEL}",),
(f"google/{DEFAULT_SYNTHESIS_MODEL}",),
]
actual_calls = [call[0] for call in mock_instructor.call_args_list]
assert actual_calls == expected_calls
def test_full_workflow_auto_config(self):
"""Test complete workflow with auto-configuration."""
api_key = "auto-config-key"
with patch.dict(os.environ, {"GEMINI_API_KEY": api_key}):
with patch("instructor.from_provider") as mock_instructor:
mock_client = MagicMock()
mock_instructor.return_value = mock_client
# Should auto-configure on first client access
client = get_critique_client()
# Should have auto-configured
assert is_configured()
assert client == mock_client
mock_instructor.assert_called_once_with(
f"google/{DEFAULT_CRITIQUE_MODEL}", async_client=True
)
def test_mixed_configuration_sources(self):
"""Test behavior with both explicit config and environment variables."""
env_key = "env-key"
explicit_key = "explicit-key"
# Set environment variable
with patch.dict(os.environ, {"GEMINI_API_KEY": env_key}):
# But configure explicitly with different key
configure(explicit_key)
# Explicit configuration should take precedence
assert os.environ.get("GEMINI_API_KEY") == explicit_key
assert is_configured()