A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
378 lines
13 KiB
Python
378 lines
13 KiB
Python
"""
|
|
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)
|