"""Phase 1 tests: LLM client. Covers: - Async HTTP-based LLM client for Qwen LLM - Provider switching via Settings - Error handling for API failures - Mocked responses in test mode """ import asyncio import pytest import httpx from unittest.mock import AsyncMock from app.services.llm_client import LLMClient, LLMClientError from app.core.config import get_settings class TestLLMClient: """LLM client tests (external calls mocked).""" @pytest.mark.asyncio async def test_llm_call_success(self, monkeypatch): """Should return content from mocked LLM API.""" from unittest.mock import MagicMock, AsyncMock settings = get_settings() client = LLMClient(settings) mock_message = MagicMock() mock_message.content = "mock response" mock_choice = MagicMock() mock_choice.message = mock_message mock_usage = MagicMock() mock_usage.prompt_tokens = 10 mock_usage.completion_tokens = 5 mock_response = MagicMock() mock_response.choices = [mock_choice] mock_response.usage = mock_usage client._client.chat.completions.create = AsyncMock(return_value=mock_response) result = await client.complete(prompt="test prompt", temperature=0.7, step_name="TestStep") assert isinstance(result, str) assert "mock" in result def test_llm_provider_switching(self): settings = get_settings() client = LLMClient(settings) assert str(client._client.base_url).rstrip("/") == settings.llm_base_url.rstrip("/") @pytest.mark.asyncio async def test_llm_api_error_handling(self, monkeypatch): settings = get_settings() client = LLMClient(settings) async def _mock_post(*args, **kwargs): # type: ignore raise httpx.HTTPStatusError("err", request=None, response=None) # type: ignore client._client.post = _mock_post # type: ignore with pytest.raises(LLMClientError): await client.complete(prompt="test", temperature=0.7)