legco_ai_assistant/backend/app/test/test_phase1_llm_client.py

60 lines
2.0 KiB
Python

"""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)