""" Tests fuer LLM Comparison Route. """ import pytest from unittest.mock import AsyncMock, patch, MagicMock from datetime import datetime class TestComparisonModels: """Tests fuer die Pydantic Models.""" def test_comparison_request_defaults(self): """Test ComparisonRequest mit Default-Werten.""" from llm_gateway.routes.comparison import ComparisonRequest req = ComparisonRequest(prompt="Test prompt") assert req.prompt == "Test prompt" assert req.system_prompt is None assert req.enable_openai is True assert req.enable_claude is True assert req.enable_selfhosted_tavily is True assert req.enable_selfhosted_edusearch is True assert req.selfhosted_model == "llama3.2:3b" assert req.temperature == 0.7 assert req.top_p == 0.9 assert req.max_tokens == 2048 assert req.search_results_count == 5 def test_comparison_request_custom_values(self): """Test ComparisonRequest mit benutzerdefinierten Werten.""" from llm_gateway.routes.comparison import ComparisonRequest req = ComparisonRequest( prompt="Custom prompt", system_prompt="Du bist ein Experte", enable_openai=False, enable_claude=True, enable_selfhosted_tavily=False, enable_selfhosted_edusearch=True, selfhosted_model="llama3.1:8b", temperature=0.5, top_p=0.8, max_tokens=1024, search_results_count=10, edu_search_filters={"language": ["de"], "doc_type": ["Lehrplan"]}, ) assert req.prompt == "Custom prompt" assert req.system_prompt == "Du bist ein Experte" assert req.enable_openai is False assert req.selfhosted_model == "llama3.1:8b" assert req.temperature == 0.5 assert req.edu_search_filters == {"language": ["de"], "doc_type": ["Lehrplan"]} def test_llm_response_model(self): """Test LLMResponse Model.""" from llm_gateway.routes.comparison import LLMResponse response = LLMResponse( provider="openai", model="gpt-4o-mini", response="Test response", latency_ms=500, tokens_used=100, ) assert response.provider == "openai" assert response.model == "gpt-4o-mini" assert response.response == "Test response" assert response.latency_ms == 500 assert response.tokens_used == 100 assert response.error is None assert response.search_results is None def test_llm_response_with_error(self): """Test LLMResponse mit Fehler.""" from llm_gateway.routes.comparison import LLMResponse response = LLMResponse( provider="claude", model="claude-3-5-sonnet", response="", latency_ms=100, error="API Key nicht konfiguriert", ) assert response.error == "API Key nicht konfiguriert" assert response.response == "" def test_llm_response_with_search_results(self): """Test LLMResponse mit Suchergebnissen.""" from llm_gateway.routes.comparison import LLMResponse search_results = [ {"title": "Lehrplan Mathe", "url": "https://example.com", "content": "..."}, {"title": "Bildungsstandards", "url": "https://kmk.org", "content": "..."}, ] response = LLMResponse( provider="selfhosted_edusearch", model="llama3.2:3b", response="Antwort mit Quellen", latency_ms=2000, search_results=search_results, ) assert len(response.search_results) == 2 assert response.search_results[0]["title"] == "Lehrplan Mathe" class TestComparisonResponse: """Tests fuer ComparisonResponse.""" def test_comparison_response_structure(self): """Test ComparisonResponse Struktur.""" from llm_gateway.routes.comparison import ComparisonResponse, LLMResponse responses = [ LLMResponse( provider="openai", model="gpt-4o-mini", response="OpenAI Antwort", latency_ms=400, ), LLMResponse( provider="claude", model="claude-3-5-sonnet", response="Claude Antwort", latency_ms=600, ), ] result = ComparisonResponse( comparison_id="cmp-test123", prompt="Was ist 1+1?", system_prompt="Du bist ein Mathe-Lehrer", responses=responses, ) assert result.comparison_id == "cmp-test123" assert result.prompt == "Was ist 1+1?" assert result.system_prompt == "Du bist ein Mathe-Lehrer" assert len(result.responses) == 2 assert result.responses[0].provider == "openai" assert result.responses[1].provider == "claude" class TestSystemPromptStore: """Tests fuer System Prompt Management.""" def test_default_system_prompts_exist(self): """Test dass Standard-Prompts existieren.""" from llm_gateway.routes.comparison import _system_prompts_store assert "default" in _system_prompts_store assert "curriculum" in _system_prompts_store assert "worksheet" in _system_prompts_store def test_default_prompt_structure(self): """Test Struktur der Standard-Prompts.""" from llm_gateway.routes.comparison import _system_prompts_store default = _system_prompts_store["default"] assert "id" in default assert "name" in default assert "prompt" in default assert "created_at" in default assert default["id"] == "default" assert "Lehrer" in default["prompt"] or "Assistent" in default["prompt"] class TestSearchFunctions: """Tests fuer Such-Funktionen.""" @pytest.mark.asyncio async def test_search_tavily_no_api_key(self): """Test Tavily Suche ohne API Key.""" from llm_gateway.routes.comparison import _search_tavily with patch.dict("os.environ", {}, clear=True): results = await _search_tavily("test query", 5) assert results == [] @pytest.mark.asyncio async def test_search_edusearch_connection_error(self): """Test EduSearch bei Verbindungsfehler.""" from llm_gateway.routes.comparison import _search_edusearch with patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.__aenter__ = AsyncMock(return_value=mock_instance) mock_instance.__aexit__ = AsyncMock(return_value=None) mock_instance.post = AsyncMock(side_effect=Exception("Connection refused")) mock_client.return_value = mock_instance results = await _search_edusearch("test query", 5) assert results == [] class TestLLMCalls: """Tests fuer LLM-Aufrufe.""" @pytest.mark.asyncio async def test_call_openai_no_api_key(self): """Test OpenAI Aufruf ohne API Key.""" from llm_gateway.routes.comparison import _call_openai with patch.dict("os.environ", {}, clear=True): result = await _call_openai("Test prompt", None) assert result.provider == "openai" assert result.error is not None assert "OPENAI_API_KEY" in result.error @pytest.mark.asyncio async def test_call_claude_no_api_key(self): """Test Claude Aufruf ohne API Key.""" from llm_gateway.routes.comparison import _call_claude with patch.dict("os.environ", {}, clear=True): result = await _call_claude("Test prompt", None) assert result.provider == "claude" assert result.error is not None assert "ANTHROPIC_API_KEY" in result.error class TestComparisonEndpoints: """Integration Tests fuer die API Endpoints.""" @pytest.fixture def mock_verify_api_key(self): """Mock fuer API Key Verifizierung.""" with patch("llm_gateway.routes.comparison.verify_api_key") as mock: mock.return_value = "test-user" yield mock @pytest.mark.asyncio async def test_list_system_prompts(self, mock_verify_api_key): """Test GET /comparison/prompts.""" from llm_gateway.routes.comparison import list_system_prompts result = await list_system_prompts(_="test-user") assert "prompts" in result assert len(result["prompts"]) >= 3 # default, curriculum, worksheet @pytest.mark.asyncio async def test_get_system_prompt(self, mock_verify_api_key): """Test GET /comparison/prompts/{prompt_id}.""" from llm_gateway.routes.comparison import get_system_prompt result = await get_system_prompt("default", _="test-user") assert result["id"] == "default" assert "name" in result assert "prompt" in result @pytest.mark.asyncio async def test_get_system_prompt_not_found(self, mock_verify_api_key): """Test GET /comparison/prompts/{prompt_id} mit unbekannter ID.""" from fastapi import HTTPException from llm_gateway.routes.comparison import get_system_prompt with pytest.raises(HTTPException) as exc_info: await get_system_prompt("nonexistent-id", _="test-user") assert exc_info.value.status_code == 404 @pytest.mark.asyncio async def test_get_comparison_history(self, mock_verify_api_key): """Test GET /comparison/history.""" from llm_gateway.routes.comparison import get_comparison_history result = await get_comparison_history(limit=10, _="test-user") assert "comparisons" in result assert isinstance(result["comparisons"], list) class TestProviderMapping: """Tests fuer Provider-Label und -Color Mapping.""" def test_provider_labels(self): """Test dass alle Provider Labels haben.""" provider_labels = { "openai": "OpenAI GPT-4o-mini", "claude": "Claude 3.5 Sonnet", "selfhosted_tavily": "Self-hosted + Tavily", "selfhosted_edusearch": "Self-hosted + EduSearch", } for key, expected in provider_labels.items(): assert key in ["openai", "claude", "selfhosted_tavily", "selfhosted_edusearch"] class TestParameterValidation: """Tests fuer Parameter-Validierung.""" def test_temperature_range(self): """Test Temperature Bereich 0-2.""" from llm_gateway.routes.comparison import ComparisonRequest from pydantic import ValidationError # Gueltige Werte req = ComparisonRequest(prompt="test", temperature=0.0) assert req.temperature == 0.0 req = ComparisonRequest(prompt="test", temperature=2.0) assert req.temperature == 2.0 # Ungueltige Werte with pytest.raises(ValidationError): ComparisonRequest(prompt="test", temperature=-0.1) with pytest.raises(ValidationError): ComparisonRequest(prompt="test", temperature=2.1) def test_top_p_range(self): """Test Top-P Bereich 0-1.""" from llm_gateway.routes.comparison import ComparisonRequest from pydantic import ValidationError # Gueltige Werte req = ComparisonRequest(prompt="test", top_p=0.0) assert req.top_p == 0.0 req = ComparisonRequest(prompt="test", top_p=1.0) assert req.top_p == 1.0 # Ungueltige Werte with pytest.raises(ValidationError): ComparisonRequest(prompt="test", top_p=-0.1) with pytest.raises(ValidationError): ComparisonRequest(prompt="test", top_p=1.1) def test_max_tokens_range(self): """Test Max Tokens Bereich 1-8192.""" from llm_gateway.routes.comparison import ComparisonRequest from pydantic import ValidationError # Gueltige Werte req = ComparisonRequest(prompt="test", max_tokens=1) assert req.max_tokens == 1 req = ComparisonRequest(prompt="test", max_tokens=8192) assert req.max_tokens == 8192 # Ungueltige Werte with pytest.raises(ValidationError): ComparisonRequest(prompt="test", max_tokens=0) with pytest.raises(ValidationError): ComparisonRequest(prompt="test", max_tokens=8193) def test_search_results_count_range(self): """Test Search Results Count Bereich 1-20.""" from llm_gateway.routes.comparison import ComparisonRequest from pydantic import ValidationError # Gueltige Werte req = ComparisonRequest(prompt="test", search_results_count=1) assert req.search_results_count == 1 req = ComparisonRequest(prompt="test", search_results_count=20) assert req.search_results_count == 20 # Ungueltige Werte with pytest.raises(ValidationError): ComparisonRequest(prompt="test", search_results_count=0) with pytest.raises(ValidationError): ComparisonRequest(prompt="test", search_results_count=21)