DeepBoner / tests /unit /clients /test_chat_client_factory.py
VibecoderMcSwaggins's picture
refactor(orchestrator): Introduce WorkflowState dataclass (Priority 7) (#129)
e85ccf5 unverified
"""Unit tests for ChatClientFactory (SPEC-16: Unified Architecture)."""
from unittest.mock import MagicMock, patch
import pytest
from src.utils.exceptions import ConfigurationError
# Skip if agent-framework-core not installed
pytest.importorskip("agent_framework")
@pytest.mark.unit
class TestChatClientFactory:
"""Test get_chat_client() factory function."""
def test_returns_openai_client_when_openai_key_available(self) -> None:
"""When OpenAI key is available, should return OpenAIChatClient."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = True
mock_settings.has_gemini_key = False
mock_settings.openai_api_key = "sk-test-key"
mock_settings.openai_model = "gpt-5"
from src.clients.factory import get_chat_client
client = get_chat_client()
# Should be OpenAIChatClient
assert "OpenAI" in type(client).__name__
def test_returns_huggingface_client_when_no_key_available(self) -> None:
"""When no API key is available, should return HuggingFaceChatClient (free tier)."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False
mock_settings.has_gemini_key = False
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from src.clients.factory import get_chat_client
client = get_chat_client()
# Should be HuggingFaceChatClient
assert "HuggingFace" in type(client).__name__
def test_explicit_provider_openai_overrides_auto_detection(self) -> None:
"""Explicit provider='openai' should use OpenAI even if no env key."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False
mock_settings.has_gemini_key = False
mock_settings.openai_api_key = None
mock_settings.openai_model = "gpt-5"
from src.clients.factory import get_chat_client
# Explicit provider with api_key parameter
client = get_chat_client(provider="openai", api_key="sk-explicit-key")
assert "OpenAI" in type(client).__name__
def test_explicit_provider_huggingface(self) -> None:
"""Explicit provider='huggingface' should use HuggingFace."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = True # Even with OpenAI key available
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from src.clients.factory import get_chat_client
# Explicit provider forces HuggingFace
client = get_chat_client(provider="huggingface")
assert "HuggingFace" in type(client).__name__
def test_unsupported_provider_raises_configuration_error(self) -> None:
"""Unsupported provider should raise ConfigurationError, not silently fallback."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False
mock_settings.has_gemini_key = False
from src.clients.factory import get_chat_client
with pytest.raises(ConfigurationError, match="No suitable provider found"):
get_chat_client(provider="invalid_provider")
def test_byok_auto_detects_openai_from_key_prefix(self) -> None:
"""BYOK: api_key starting with 'sk-' should auto-select OpenAI without explicit provider.
This is the critical BYOK (Bring Your Own Key) test case:
- User enters 'sk-...' key in Gradio
- No explicit provider parameter
- No OPENAI_API_KEY in env (settings.has_openai_key = False)
- Should auto-detect OpenAI from the key prefix
"""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False # No env key
mock_settings.has_gemini_key = False
mock_settings.openai_api_key = None
mock_settings.openai_model = "gpt-5"
from src.clients.factory import get_chat_client
# BYOK: Pass api_key without explicit provider
client = get_chat_client(api_key="sk-user-provided-key")
# Should auto-detect OpenAI from 'sk-' prefix
assert "OpenAI" in type(client).__name__
def test_byok_hf_token_falls_through_to_huggingface(self) -> None:
"""BYOK: HuggingFace tokens (hf_...) should use HuggingFace client."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False
mock_settings.has_gemini_key = False
mock_settings.huggingface_model = "Qwen/Qwen2.5-7B-Instruct"
mock_settings.hf_token = None
from src.clients.factory import get_chat_client
# HF tokens don't trigger auto-detection, falls through to HuggingFace
client = get_chat_client(api_key="hf_user_provided_token")
assert "HuggingFace" in type(client).__name__
def test_provider_is_case_insensitive(self) -> None:
"""Provider matching should be case-insensitive."""
with patch("src.clients.factory.settings") as mock_settings:
mock_settings.has_openai_key = False
mock_settings.has_gemini_key = False
mock_settings.openai_api_key = None
mock_settings.openai_model = "gpt-5"
from src.clients.factory import get_chat_client
# "OpenAI" should work same as "openai"
client = get_chat_client(provider="OpenAI", api_key="sk-test")
assert "OpenAI" in type(client).__name__
# "HUGGINGFACE" should work same as "huggingface"
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
client = get_chat_client(provider="HUGGINGFACE")
assert "HuggingFace" in type(client).__name__
@pytest.mark.unit
class TestHuggingFaceChatClient:
"""Test HuggingFaceChatClient adapter."""
def test_initialization_with_defaults(self) -> None:
"""Should initialize with default model from settings."""
with patch("src.clients.huggingface.settings") as mock_settings:
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from src.clients.huggingface import HuggingFaceChatClient
client = HuggingFaceChatClient()
assert client.model_id == "meta-llama/Llama-3.1-70B-Instruct"
def test_initialization_with_custom_model(self) -> None:
"""Should accept custom model_id."""
with patch("src.clients.huggingface.settings") as mock_settings:
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from src.clients.huggingface import HuggingFaceChatClient
client = HuggingFaceChatClient(model_id="mistralai/Mistral-7B-Instruct-v0.3")
assert client.model_id == "mistralai/Mistral-7B-Instruct-v0.3"
def test_convert_messages_basic(self) -> None:
"""Should convert ChatMessage list to HuggingFace format."""
with patch("src.clients.huggingface.settings") as mock_settings:
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from agent_framework import ChatMessage
from src.clients.huggingface import HuggingFaceChatClient
client = HuggingFaceChatClient()
# Create mock messages (include contents=None for tool call processing)
messages = [
MagicMock(spec=ChatMessage, role="user", text="Hello", contents=None),
MagicMock(spec=ChatMessage, role="assistant", text="Hi there!", contents=None),
]
result = client._convert_messages(messages)
assert len(result) == 2
assert result[0] == {"role": "user", "content": "Hello"}
assert result[1] == {"role": "assistant", "content": "Hi there!"}
def test_convert_messages_handles_role_enum(self) -> None:
"""Should extract .value from Role enum, not stringify the enum itself."""
with patch("src.clients.huggingface.settings") as mock_settings:
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from enum import Enum
from agent_framework import ChatMessage
from src.clients.huggingface import HuggingFaceChatClient
# Simulate a Role enum like agent_framework might use
class Role(Enum):
USER = "user"
ASSISTANT = "assistant"
client = HuggingFaceChatClient()
# Create mock message with enum role
mock_msg = MagicMock(spec=ChatMessage)
mock_msg.role = Role.USER # Enum, not string
mock_msg.text = "Hello"
mock_msg.contents = None # Required for tool call processing
result = client._convert_messages([mock_msg])
# Should be "user", NOT "Role.USER"
assert result[0]["role"] == "user"
assert "Role" not in result[0]["role"]
def test_inherits_from_base_chat_client(self) -> None:
"""Should inherit from agent_framework.BaseChatClient."""
with patch("src.clients.huggingface.settings") as mock_settings:
mock_settings.huggingface_model = "meta-llama/Llama-3.1-70B-Instruct"
mock_settings.hf_token = None
from agent_framework import BaseChatClient
from src.clients.huggingface import HuggingFaceChatClient
client = HuggingFaceChatClient()
assert isinstance(client, BaseChatClient)