Files
my-pal-mcp-server/tests/test_zen_provider.py

362 lines
14 KiB
Python

"""Tests for OpenCode Zen provider."""
import os
from unittest.mock import Mock, patch
import pytest
from providers.registry import ModelProviderRegistry
from providers.shared import ProviderType
from providers.zen import ZenProvider
class TestZenProvider:
"""Test cases for OpenCode Zen provider."""
def test_provider_initialization(self):
"""Test Zen provider initialization."""
provider = ZenProvider(api_key="test-key")
assert provider.api_key == "test-key"
assert provider.base_url == "https://opencode.ai/zen/v1"
assert provider.FRIENDLY_NAME == "OpenCode Zen"
def test_get_provider_type(self):
"""Test provider type identification."""
provider = ZenProvider(api_key="test-key")
assert provider.get_provider_type() == ProviderType.ZEN
def test_model_validation(self):
"""Test model validation."""
provider = ZenProvider(api_key="test-key")
# Zen accepts models that are in the registry
assert provider.validate_model_name("claude-sonnet-4-5") is True
assert provider.validate_model_name("gpt-5.1-codex") is True
# Unknown models are rejected
assert provider.validate_model_name("unknown-model") is False
def test_get_capabilities(self):
"""Test capability generation."""
provider = ZenProvider(api_key="test-key")
# Test with a model in the registry
caps = provider.get_capabilities("claude-sonnet-4-5")
assert caps.provider == ProviderType.ZEN
assert caps.model_name == "claude-sonnet-4-5"
assert caps.friendly_name == "OpenCode Zen (claude-sonnet-4-5)"
# Test with a model not in registry - should raise error
with pytest.raises(ValueError, match="Unsupported model 'unknown-model' for provider zen"):
provider.get_capabilities("unknown-model")
def test_model_alias_resolution(self):
"""Test model alias resolution."""
provider = ZenProvider(api_key="test-key")
# Test alias resolution
assert provider._resolve_model_name("zen-sonnet") == "claude-sonnet-4-5"
assert provider._resolve_model_name("zen-sonnet4.5") == "claude-sonnet-4-5"
assert provider._resolve_model_name("zen-codex") == "gpt-5.1-codex"
assert provider._resolve_model_name("zen-gpt-codex") == "gpt-5.1-codex"
# Test case-insensitive
assert provider._resolve_model_name("ZEN-SONNET") == "claude-sonnet-4-5"
assert provider._resolve_model_name("Zen-Codex") == "gpt-5.1-codex"
# Test direct model names (should pass through unchanged)
assert provider._resolve_model_name("claude-sonnet-4-5") == "claude-sonnet-4-5"
assert provider._resolve_model_name("gpt-5.1-codex") == "gpt-5.1-codex"
# Test unknown models pass through
assert provider._resolve_model_name("unknown-model") == "unknown-model"
def test_list_models(self):
"""Test model listing with various options."""
provider = ZenProvider(api_key="test-key")
# Test basic model listing
models = provider.list_models()
assert isinstance(models, list)
assert len(models) > 0
# Should include our configured models
assert "claude-sonnet-4-5" in models
assert "gpt-5.1-codex" in models
# Should include aliases
assert "zen-sonnet" in models
assert "zen-codex" in models
def test_list_models_with_options(self):
"""Test model listing with different options."""
provider = ZenProvider(api_key="test-key")
# Test without aliases
models_no_aliases = provider.list_models(include_aliases=False)
assert "zen-sonnet" not in models_no_aliases
assert "claude-sonnet-4-5" in models_no_aliases
# Test lowercase
models_lower = provider.list_models(lowercase=True)
assert all(model == model.lower() for model in models_lower)
def test_registry_capabilities(self):
"""Test that registry capabilities are properly loaded."""
provider = ZenProvider(api_key="test-key")
# Test that we have a registry
assert provider._registry is not None
# Test getting all capabilities
capabilities = provider.get_all_model_capabilities()
assert isinstance(capabilities, dict)
assert len(capabilities) > 0
# Should include our configured models
assert "claude-sonnet-4-5" in capabilities
assert "gpt-5.1-codex" in capabilities
# Check capability structure
caps = capabilities["claude-sonnet-4-5"]
assert caps.provider == ProviderType.ZEN
assert caps.context_window == 200000
assert caps.intelligence_score == 17
def test_model_capabilities_lookup(self):
"""Test capability lookup for known and unknown models."""
provider = ZenProvider(api_key="test-key")
# Test known model
caps = provider._lookup_capabilities("claude-sonnet-4-5")
assert caps is not None
assert caps.provider == ProviderType.ZEN
# Test unknown model returns None (base class handles error)
caps = provider._lookup_capabilities("unknown-zen-model")
assert caps is None
def test_zen_registration(self):
"""Test Zen can be registered and retrieved."""
with patch.dict(os.environ, {"ZEN_API_KEY": "test-key"}):
# Clean up any existing registration
ModelProviderRegistry.unregister_provider(ProviderType.ZEN)
# Register the provider
ModelProviderRegistry.register_provider(ProviderType.ZEN, ZenProvider)
# Retrieve and verify
provider = ModelProviderRegistry.get_provider(ProviderType.ZEN)
assert provider is not None
assert isinstance(provider, ZenProvider)
class TestZenAutoMode:
"""Test auto mode functionality when only Zen is configured."""
def setup_method(self):
"""Store original state before each test."""
self.registry = ModelProviderRegistry()
self._original_providers = self.registry._providers.copy()
self._original_initialized = self.registry._initialized_providers.copy()
# Clear the registry state for this test
self.registry._providers.clear()
self.registry._initialized_providers.clear()
self._original_env = {}
for key in ["ZEN_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "DEFAULT_MODEL"]:
self._original_env[key] = os.environ.get(key)
def teardown_method(self):
"""Restore original state after each test."""
self.registry._providers.clear()
self.registry._initialized_providers.clear()
self.registry._providers.update(self._original_providers)
self.registry._initialized_providers.update(self._original_initialized)
for key, value in self._original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
@pytest.mark.no_mock_provider
def test_zen_only_auto_mode(self):
"""Test that auto mode works when only Zen is configured."""
os.environ.pop("GEMINI_API_KEY", None)
os.environ.pop("OPENAI_API_KEY", None)
os.environ["ZEN_API_KEY"] = "test-zen-key"
os.environ["DEFAULT_MODEL"] = "auto"
mock_registry = Mock()
model_names = [
"claude-sonnet-4-5",
"claude-haiku-4-5",
"gpt-5.1-codex",
"gemini-3-pro",
"glm-4.6",
]
mock_registry.list_models.return_value = model_names
# Mock resolve to return a ModelCapabilities-like object for each model
def mock_resolve(model_name):
if model_name in model_names:
mock_config = Mock()
mock_config.provider = ProviderType.ZEN
mock_config.aliases = [] # Empty list of aliases
mock_config.get_effective_capability_rank = Mock(return_value=50) # Add ranking method
return mock_config
return None
mock_registry.resolve.side_effect = mock_resolve
ModelProviderRegistry.register_provider(ProviderType.ZEN, ZenProvider)
provider = ModelProviderRegistry.get_provider(ProviderType.ZEN)
assert provider is not None, "Zen provider should be available with API key"
provider._registry = mock_registry
available_models = ModelProviderRegistry.get_available_models(respect_restrictions=True)
assert len(available_models) > 0, "Should find Zen models in auto mode"
assert all(provider_type == ProviderType.ZEN for provider_type in available_models.values())
for model in model_names:
assert model in available_models, f"Model {model} should be available"
class TestZenIntegration:
"""Integration tests for Zen provider with server components."""
def test_zen_provider_in_server_init(self):
"""Test that Zen provider is properly handled during server initialization."""
# This test verifies that the server can handle Zen provider configuration
# without actual server startup
with patch.dict(os.environ, {"ZEN_API_KEY": "test-integration-key"}):
# Import server module to trigger provider setup
from providers.registry import ModelProviderRegistry
# Verify Zen provider can be registered
ModelProviderRegistry.register_provider(ProviderType.ZEN, ZenProvider)
provider = ModelProviderRegistry.get_provider(ProviderType.ZEN)
assert provider is not None
assert isinstance(provider, ZenProvider)
def test_zen_config_loading(self):
"""Test that Zen configuration loads properly in integration context."""
with patch.dict(os.environ, {"ZEN_API_KEY": "test-config-key"}):
from providers.registries.zen import ZenModelRegistry
# Test registry loads configuration
registry = ZenModelRegistry()
models = registry.list_models()
aliases = registry.list_aliases()
assert len(models) > 0, "Should load models from zen_models.json"
assert len(aliases) > 0, "Should load aliases from zen_models.json"
# Verify specific models are loaded
assert "claude-sonnet-4-5" in models
assert "zen-sonnet" in aliases
def test_zen_provider_priority(self):
"""Test that Zen provider follows correct priority order."""
# Zen should be prioritized after native APIs but before OpenRouter
from providers.registry import ModelProviderRegistry
priority_order = ModelProviderRegistry.PROVIDER_PRIORITY_ORDER
zen_index = priority_order.index(ProviderType.ZEN)
openrouter_index = priority_order.index(ProviderType.OPENROUTER)
# Zen should come before OpenRouter in priority
assert zen_index < openrouter_index, "Zen should have higher priority than OpenRouter"
class TestZenAPIMocking:
"""Test API interactions with mocked OpenAI SDK."""
def test_chat_completion_mock(self):
"""Test chat completion with mocked API response."""
provider = ZenProvider(api_key="test-key")
# Mock the OpenAI client and response
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message.content = "Mocked response from Zen"
mock_response.usage = Mock()
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 20
with patch.object(provider.client.chat.completions, "create", return_value=mock_response):
# Test the completion method - this will initialize the client
response = provider.complete(
model="claude-sonnet-4-5", messages=[{"role": "user", "content": "Hello"}], temperature=0.7
)
assert response.content == "Mocked response from Zen"
def test_streaming_completion_mock(self):
"""Test streaming completion with mocked API."""
provider = ZenProvider(api_key="test-key")
# Mock streaming response
mock_chunk1 = Mock()
mock_chunk1.choices = [Mock()]
mock_chunk1.choices[0].delta.content = "Hello"
mock_chunk1.choices[0].finish_reason = None
mock_chunk2 = Mock()
mock_chunk2.choices = [Mock()]
mock_chunk2.choices[0].delta.content = " world!"
mock_chunk2.choices[0].finish_reason = "stop"
mock_stream = [mock_chunk1, mock_chunk2]
# Access client to initialize it first
_ = provider.client
with patch.object(provider.client.chat.completions, "create", return_value=mock_stream):
# Test streaming completion
stream = provider.complete_stream(
model="gpt-5.1-codex",
messages=[{"role": "user", "content": "Say hello"}],
)
chunks = list(stream)
assert len(chunks) == 2
assert chunks[0].content == "Hello"
assert chunks[1].content == " world!"
def test_api_error_handling(self):
"""Test error handling for API failures."""
provider = ZenProvider(api_key="test-key")
# Mock API error
from openai import APIError
api_error = APIError("Mock API error", request=Mock(), body="error details")
with patch.object(provider._client.chat.completions, "create", side_effect=api_error):
with pytest.raises(APIError):
provider.complete(model="claude-sonnet-4-5", messages=[{"role": "user", "content": "Test"}])
def test_invalid_model_error(self):
"""Test error handling for invalid models."""
provider = ZenProvider(api_key="test-key")
with pytest.raises(ValueError, match="Unsupported model 'invalid-model' for provider zen"):
provider.get_capabilities("invalid-model")
def test_authentication_error(self):
"""Test handling of authentication errors."""
provider = ZenProvider(api_key="invalid-key")
# Mock authentication error
from openai import AuthenticationError
auth_error = AuthenticationError("Invalid API key", request=Mock(), body="auth failed")
with patch.object(provider._client.chat.completions, "create", side_effect=auth_error):
with pytest.raises(AuthenticationError):
provider.complete(model="claude-sonnet-4-5", messages=[{"role": "user", "content": "Test"}])