fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
369
backend/tests/test_llm_gateway/test_tool_gateway.py
Normal file
369
backend/tests/test_llm_gateway/test_tool_gateway.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
Tests für Tool Gateway Service.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import httpx
|
||||
|
||||
from llm_gateway.services.tool_gateway import (
|
||||
ToolGateway,
|
||||
ToolGatewayConfig,
|
||||
SearchDepth,
|
||||
SearchResult,
|
||||
SearchResponse,
|
||||
TavilyError,
|
||||
ToolGatewayError,
|
||||
get_tool_gateway,
|
||||
)
|
||||
from llm_gateway.services.pii_detector import PIIDetector, RedactionResult
|
||||
|
||||
|
||||
class TestToolGatewayConfig:
|
||||
"""Tests für ToolGatewayConfig."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Test Standardkonfiguration."""
|
||||
config = ToolGatewayConfig()
|
||||
|
||||
assert config.tavily_api_key is None
|
||||
assert config.tavily_base_url == "https://api.tavily.com"
|
||||
assert config.timeout == 30
|
||||
assert config.max_results == 5
|
||||
assert config.search_depth == SearchDepth.BASIC
|
||||
assert config.include_answer is True
|
||||
assert config.pii_redaction_enabled is True
|
||||
|
||||
def test_config_from_env(self):
|
||||
"""Test Konfiguration aus Umgebungsvariablen."""
|
||||
with patch.dict("os.environ", {
|
||||
"TAVILY_API_KEY": "test-key",
|
||||
"TAVILY_BASE_URL": "https://custom.tavily.com",
|
||||
"TAVILY_TIMEOUT": "60",
|
||||
"TAVILY_MAX_RESULTS": "10",
|
||||
"TAVILY_SEARCH_DEPTH": "advanced",
|
||||
"PII_REDACTION_ENABLED": "false",
|
||||
}):
|
||||
config = ToolGatewayConfig.from_env()
|
||||
|
||||
assert config.tavily_api_key == "test-key"
|
||||
assert config.tavily_base_url == "https://custom.tavily.com"
|
||||
assert config.timeout == 60
|
||||
assert config.max_results == 10
|
||||
assert config.search_depth == SearchDepth.ADVANCED
|
||||
assert config.pii_redaction_enabled is False
|
||||
|
||||
|
||||
class TestToolGatewayAvailability:
|
||||
"""Tests für Gateway-Verfügbarkeit."""
|
||||
|
||||
def test_tavily_not_available_without_key(self):
|
||||
"""Test Tavily nicht verfügbar ohne API Key."""
|
||||
config = ToolGatewayConfig(tavily_api_key=None)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
assert gateway.tavily_available is False
|
||||
|
||||
def test_tavily_available_with_key(self):
|
||||
"""Test Tavily verfügbar mit API Key."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
assert gateway.tavily_available is True
|
||||
|
||||
|
||||
class TestToolGatewayPIIRedaction:
|
||||
"""Tests für PII-Redaktion im Gateway."""
|
||||
|
||||
def test_redact_query_with_email(self):
|
||||
"""Test Redaktion von E-Mail in Query."""
|
||||
config = ToolGatewayConfig(pii_redaction_enabled=True)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
result = gateway._redact_query("Kontakt test@example.com Datenschutz")
|
||||
|
||||
assert result.pii_found is True
|
||||
assert "test@example.com" not in result.redacted_text
|
||||
assert "[EMAIL_REDACTED]" in result.redacted_text
|
||||
|
||||
def test_no_redaction_when_disabled(self):
|
||||
"""Test keine Redaktion wenn deaktiviert."""
|
||||
config = ToolGatewayConfig(pii_redaction_enabled=False)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
result = gateway._redact_query("test@example.com")
|
||||
|
||||
assert result.pii_found is False
|
||||
assert result.redacted_text == "test@example.com"
|
||||
|
||||
|
||||
class TestToolGatewaySearch:
|
||||
"""Tests für Suche mit Gateway."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_raises_error_without_key(self):
|
||||
"""Test Fehler bei Suche ohne API Key."""
|
||||
config = ToolGatewayConfig(tavily_api_key=None)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
with pytest.raises(ToolGatewayError, match="not configured"):
|
||||
await gateway.search("test query")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_success(self):
|
||||
"""Test erfolgreiche Suche."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"results": [
|
||||
{
|
||||
"title": "Test Result",
|
||||
"url": "https://example.com",
|
||||
"content": "Test content",
|
||||
"score": 0.95,
|
||||
}
|
||||
],
|
||||
"answer": "Test answer",
|
||||
}
|
||||
|
||||
with patch.object(gateway, "_get_client") as mock_client:
|
||||
mock_http = AsyncMock()
|
||||
mock_http.post.return_value = mock_response
|
||||
mock_client.return_value = mock_http
|
||||
|
||||
result = await gateway.search("Schulrecht Bayern")
|
||||
|
||||
assert result.query == "Schulrecht Bayern"
|
||||
assert len(result.results) == 1
|
||||
assert result.results[0].title == "Test Result"
|
||||
assert result.answer == "Test answer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_pii_redaction(self):
|
||||
"""Test Suche mit PII-Redaktion."""
|
||||
config = ToolGatewayConfig(
|
||||
tavily_api_key="test-key",
|
||||
pii_redaction_enabled=True,
|
||||
)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"results": [],
|
||||
"answer": None,
|
||||
}
|
||||
|
||||
with patch.object(gateway, "_get_client") as mock_client:
|
||||
mock_http = AsyncMock()
|
||||
mock_http.post.return_value = mock_response
|
||||
mock_client.return_value = mock_http
|
||||
|
||||
result = await gateway.search("Kontakt test@example.com Datenschutz")
|
||||
|
||||
assert result.pii_detected is True
|
||||
assert "email" in result.pii_types
|
||||
assert result.redacted_query is not None
|
||||
assert "test@example.com" not in result.redacted_query
|
||||
|
||||
# Prüfen dass redaktierte Query an Tavily gesendet wurde
|
||||
call_args = mock_http.post.call_args
|
||||
sent_query = call_args.kwargs["json"]["query"]
|
||||
assert "test@example.com" not in sent_query
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_http_error(self):
|
||||
"""Test HTTP-Fehler bei Suche."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 429
|
||||
mock_response.text = "Rate limit exceeded"
|
||||
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
||||
"Rate limit",
|
||||
request=MagicMock(),
|
||||
response=mock_response,
|
||||
)
|
||||
|
||||
with patch.object(gateway, "_get_client") as mock_client:
|
||||
mock_http = AsyncMock()
|
||||
mock_http.post.return_value = mock_response
|
||||
mock_client.return_value = mock_http
|
||||
|
||||
with pytest.raises(TavilyError, match="429"):
|
||||
await gateway.search("test")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_domain_filters(self):
|
||||
"""Test Suche mit Domain-Filtern."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"results": [], "answer": None}
|
||||
|
||||
with patch.object(gateway, "_get_client") as mock_client:
|
||||
mock_http = AsyncMock()
|
||||
mock_http.post.return_value = mock_response
|
||||
mock_client.return_value = mock_http
|
||||
|
||||
await gateway.search(
|
||||
"test",
|
||||
include_domains=["gov.de", "schule.de"],
|
||||
exclude_domains=["wikipedia.org"],
|
||||
)
|
||||
|
||||
call_args = mock_http.post.call_args
|
||||
payload = call_args.kwargs["json"]
|
||||
assert payload["include_domains"] == ["gov.de", "schule.de"]
|
||||
assert payload["exclude_domains"] == ["wikipedia.org"]
|
||||
|
||||
|
||||
class TestSearchResult:
|
||||
"""Tests für SearchResult Dataclass."""
|
||||
|
||||
def test_search_result_creation(self):
|
||||
"""Test SearchResult erstellen."""
|
||||
result = SearchResult(
|
||||
title="Test",
|
||||
url="https://example.com",
|
||||
content="Content",
|
||||
score=0.9,
|
||||
published_date="2024-01-15",
|
||||
)
|
||||
|
||||
assert result.title == "Test"
|
||||
assert result.url == "https://example.com"
|
||||
assert result.score == 0.9
|
||||
|
||||
def test_search_result_defaults(self):
|
||||
"""Test SearchResult Standardwerte."""
|
||||
result = SearchResult(
|
||||
title="Test",
|
||||
url="https://example.com",
|
||||
content="Content",
|
||||
)
|
||||
|
||||
assert result.score == 0.0
|
||||
assert result.published_date is None
|
||||
|
||||
|
||||
class TestSearchResponse:
|
||||
"""Tests für SearchResponse Dataclass."""
|
||||
|
||||
def test_search_response_creation(self):
|
||||
"""Test SearchResponse erstellen."""
|
||||
response = SearchResponse(
|
||||
query="test query",
|
||||
results=[],
|
||||
pii_detected=False,
|
||||
)
|
||||
|
||||
assert response.query == "test query"
|
||||
assert len(response.results) == 0
|
||||
assert response.pii_detected is False
|
||||
|
||||
def test_search_response_with_pii(self):
|
||||
"""Test SearchResponse mit PII."""
|
||||
response = SearchResponse(
|
||||
query="original query",
|
||||
redacted_query="redacted query",
|
||||
results=[],
|
||||
pii_detected=True,
|
||||
pii_types=["email", "phone"],
|
||||
)
|
||||
|
||||
assert response.pii_detected is True
|
||||
assert "email" in response.pii_types
|
||||
|
||||
|
||||
class TestSearchDepthEnum:
|
||||
"""Tests für SearchDepth Enum."""
|
||||
|
||||
def test_search_depth_values(self):
|
||||
"""Test SearchDepth Werte."""
|
||||
assert SearchDepth.BASIC.value == "basic"
|
||||
assert SearchDepth.ADVANCED.value == "advanced"
|
||||
|
||||
|
||||
class TestGetToolGatewaySingleton:
|
||||
"""Tests für Singleton Pattern."""
|
||||
|
||||
def test_singleton_returns_same_instance(self):
|
||||
"""Test dass get_tool_gateway Singleton zurückgibt."""
|
||||
gateway1 = get_tool_gateway()
|
||||
gateway2 = get_tool_gateway()
|
||||
assert gateway1 is gateway2
|
||||
|
||||
|
||||
class TestToolGatewayHealthCheck:
|
||||
"""Tests für Health Check."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_without_tavily(self):
|
||||
"""Test Health Check ohne Tavily."""
|
||||
config = ToolGatewayConfig(tavily_api_key=None)
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
status = await gateway.health_check()
|
||||
|
||||
assert status["tavily"]["configured"] is False
|
||||
assert status["tavily"]["healthy"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_with_tavily_success(self):
|
||||
"""Test Health Check mit erfolgreichem Tavily."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
with patch.object(gateway, "search") as mock_search:
|
||||
mock_search.return_value = SearchResponse(
|
||||
query="test",
|
||||
results=[],
|
||||
response_time_ms=100,
|
||||
)
|
||||
|
||||
status = await gateway.health_check()
|
||||
|
||||
assert status["tavily"]["configured"] is True
|
||||
assert status["tavily"]["healthy"] is True
|
||||
assert status["tavily"]["response_time_ms"] == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_with_tavily_failure(self):
|
||||
"""Test Health Check mit Tavily-Fehler."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
with patch.object(gateway, "search") as mock_search:
|
||||
mock_search.side_effect = TavilyError("Connection failed")
|
||||
|
||||
status = await gateway.health_check()
|
||||
|
||||
assert status["tavily"]["configured"] is True
|
||||
assert status["tavily"]["healthy"] is False
|
||||
assert "error" in status["tavily"]
|
||||
|
||||
|
||||
class TestToolGatewayClose:
|
||||
"""Tests für Gateway-Cleanup."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_client(self):
|
||||
"""Test Client-Cleanup."""
|
||||
config = ToolGatewayConfig(tavily_api_key="test-key")
|
||||
gateway = ToolGateway(config=config)
|
||||
|
||||
# Simuliere dass Client erstellt wurde
|
||||
mock_client = AsyncMock()
|
||||
gateway._client = mock_client
|
||||
|
||||
await gateway.close()
|
||||
|
||||
mock_client.aclose.assert_called_once()
|
||||
assert gateway._client is None
|
||||
Reference in New Issue
Block a user