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:
501
backend/tests/test_llm_gateway/test_communication_service.py
Normal file
501
backend/tests/test_llm_gateway/test_communication_service.py
Normal file
@@ -0,0 +1,501 @@
|
||||
"""
|
||||
Tests für den Communication Service.
|
||||
|
||||
Testet die KI-gestützte Lehrer-Eltern-Kommunikation mit GFK-Prinzipien.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
|
||||
from llm_gateway.services.communication_service import (
|
||||
CommunicationService,
|
||||
CommunicationType,
|
||||
CommunicationTone,
|
||||
LegalReference,
|
||||
GFKPrinciple,
|
||||
get_communication_service,
|
||||
fetch_legal_references_from_db,
|
||||
parse_db_references_to_legal_refs,
|
||||
FALLBACK_LEGAL_REFERENCES,
|
||||
GFK_PRINCIPLES,
|
||||
)
|
||||
|
||||
|
||||
class TestCommunicationType:
|
||||
"""Tests für CommunicationType Enum."""
|
||||
|
||||
def test_all_communication_types_exist(self):
|
||||
"""Test alle erwarteten Kommunikationstypen existieren."""
|
||||
expected_types = [
|
||||
"general_info",
|
||||
"behavior",
|
||||
"academic",
|
||||
"attendance",
|
||||
"meeting_invite",
|
||||
"positive_feedback",
|
||||
"concern",
|
||||
"conflict",
|
||||
"special_needs",
|
||||
]
|
||||
actual_types = [ct.value for ct in CommunicationType]
|
||||
assert set(expected_types) == set(actual_types)
|
||||
|
||||
def test_communication_type_is_string_enum(self):
|
||||
"""Test CommunicationType ist String Enum."""
|
||||
assert CommunicationType.BEHAVIOR == "behavior"
|
||||
assert CommunicationType.ACADEMIC.value == "academic"
|
||||
|
||||
|
||||
class TestCommunicationTone:
|
||||
"""Tests für CommunicationTone Enum."""
|
||||
|
||||
def test_all_tones_exist(self):
|
||||
"""Test alle Tonalitäten existieren."""
|
||||
expected_tones = ["formal", "professional", "warm", "concerned", "appreciative"]
|
||||
actual_tones = [t.value for t in CommunicationTone]
|
||||
assert set(expected_tones) == set(actual_tones)
|
||||
|
||||
|
||||
class TestLegalReference:
|
||||
"""Tests für LegalReference Dataclass."""
|
||||
|
||||
def test_legal_reference_creation(self):
|
||||
"""Test LegalReference erstellen."""
|
||||
ref = LegalReference(
|
||||
law="SchulG NRW",
|
||||
paragraph="§ 42",
|
||||
title="Pflichten der Eltern",
|
||||
summary="Eltern unterstützen die Schule.",
|
||||
relevance="Kooperationsaufforderungen",
|
||||
)
|
||||
assert ref.law == "SchulG NRW"
|
||||
assert ref.paragraph == "§ 42"
|
||||
assert "Eltern" in ref.title
|
||||
|
||||
|
||||
class TestGFKPrinciple:
|
||||
"""Tests für GFKPrinciple Dataclass."""
|
||||
|
||||
def test_gfk_principle_creation(self):
|
||||
"""Test GFKPrinciple erstellen."""
|
||||
principle = GFKPrinciple(
|
||||
principle="Beobachtung",
|
||||
description="Konkrete Handlungen beschreiben",
|
||||
example="Ich habe bemerkt...",
|
||||
)
|
||||
assert principle.principle == "Beobachtung"
|
||||
assert "beschreiben" in principle.description
|
||||
|
||||
|
||||
class TestFallbackLegalReferences:
|
||||
"""Tests für die Fallback-Referenzen."""
|
||||
|
||||
def test_default_references_exist(self):
|
||||
"""Test DEFAULT Referenzen existieren."""
|
||||
assert "DEFAULT" in FALLBACK_LEGAL_REFERENCES
|
||||
assert "elternpflichten" in FALLBACK_LEGAL_REFERENCES["DEFAULT"]
|
||||
assert "schulpflicht" in FALLBACK_LEGAL_REFERENCES["DEFAULT"]
|
||||
|
||||
def test_fallback_references_are_legal_reference(self):
|
||||
"""Test Fallback Referenzen sind LegalReference Objekte."""
|
||||
ref = FALLBACK_LEGAL_REFERENCES["DEFAULT"]["elternpflichten"]
|
||||
assert isinstance(ref, LegalReference)
|
||||
assert ref.law == "Landesschulgesetz"
|
||||
|
||||
|
||||
class TestGFKPrinciples:
|
||||
"""Tests für GFK-Prinzipien."""
|
||||
|
||||
def test_four_gfk_principles_exist(self):
|
||||
"""Test alle 4 GFK-Prinzipien existieren."""
|
||||
assert len(GFK_PRINCIPLES) == 4
|
||||
principles = [p.principle for p in GFK_PRINCIPLES]
|
||||
assert "Beobachtung" in principles
|
||||
assert "Gefühle" in principles
|
||||
assert "Bedürfnisse" in principles
|
||||
assert "Bitten" in principles
|
||||
|
||||
|
||||
class TestCommunicationService:
|
||||
"""Tests für CommunicationService Klasse."""
|
||||
|
||||
def test_service_initialization(self):
|
||||
"""Test Service wird korrekt initialisiert."""
|
||||
service = CommunicationService()
|
||||
assert service.fallback_references is not None
|
||||
assert service.gfk_principles is not None
|
||||
assert service.templates is not None
|
||||
assert service._cached_references == {}
|
||||
|
||||
def test_get_legal_references_sync(self):
|
||||
"""Test synchrone get_legal_references Methode (Fallback)."""
|
||||
service = CommunicationService()
|
||||
refs = service.get_legal_references("NRW", "elternpflichten")
|
||||
|
||||
assert len(refs) > 0
|
||||
assert isinstance(refs[0], LegalReference)
|
||||
|
||||
def test_get_fallback_references(self):
|
||||
"""Test _get_fallback_references Methode."""
|
||||
service = CommunicationService()
|
||||
refs = service._get_fallback_references("DEFAULT", "elternpflichten")
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0].law == "Landesschulgesetz"
|
||||
|
||||
def test_get_gfk_guidance(self):
|
||||
"""Test get_gfk_guidance gibt GFK-Prinzipien zurück."""
|
||||
service = CommunicationService()
|
||||
guidance = service.get_gfk_guidance(CommunicationType.BEHAVIOR)
|
||||
|
||||
assert len(guidance) == 4
|
||||
assert all(isinstance(g, GFKPrinciple) for g in guidance)
|
||||
|
||||
def test_get_template(self):
|
||||
"""Test get_template gibt Vorlage zurück."""
|
||||
service = CommunicationService()
|
||||
template = service.get_template(CommunicationType.MEETING_INVITE)
|
||||
|
||||
assert "subject" in template
|
||||
assert "opening" in template
|
||||
assert "closing" in template
|
||||
assert "Einladung" in template["subject"]
|
||||
|
||||
def test_get_template_fallback(self):
|
||||
"""Test get_template Fallback zu GENERAL_INFO."""
|
||||
service = CommunicationService()
|
||||
# Unbekannter Typ sollte auf GENERAL_INFO fallen
|
||||
template = service.get_template(CommunicationType.GENERAL_INFO)
|
||||
|
||||
assert "Information" in template["subject"]
|
||||
|
||||
|
||||
class TestCommunicationServiceAsync:
|
||||
"""Async Tests für CommunicationService."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_legal_references_async_with_db(self):
|
||||
"""Test async get_legal_references_async mit DB-Daten."""
|
||||
service = CommunicationService()
|
||||
|
||||
# Mock die fetch_legal_references_from_db Funktion
|
||||
mock_docs = [
|
||||
{
|
||||
"law_name": "SchulG NRW",
|
||||
"title": "Schulgesetz NRW",
|
||||
"paragraphs": [
|
||||
{"nr": "§ 42", "title": "Pflichten der Eltern"},
|
||||
{"nr": "§ 41", "title": "Schulpflicht"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
with patch(
|
||||
"llm_gateway.services.communication_service.fetch_legal_references_from_db",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_fetch:
|
||||
mock_fetch.return_value = mock_docs
|
||||
|
||||
refs = await service.get_legal_references_async("NRW", "elternpflichten")
|
||||
|
||||
assert len(refs) > 0
|
||||
mock_fetch.assert_called_once_with("NRW")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_legal_references_async_fallback(self):
|
||||
"""Test async get_legal_references_async Fallback wenn DB leer."""
|
||||
service = CommunicationService()
|
||||
|
||||
with patch(
|
||||
"llm_gateway.services.communication_service.fetch_legal_references_from_db",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_fetch:
|
||||
mock_fetch.return_value = [] # Leere DB
|
||||
|
||||
refs = await service.get_legal_references_async("NRW", "elternpflichten")
|
||||
|
||||
# Sollte Fallback nutzen
|
||||
assert len(refs) > 0
|
||||
assert refs[0].law == "Landesschulgesetz"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_legal_references_async_caching(self):
|
||||
"""Test dass Ergebnisse gecached werden."""
|
||||
service = CommunicationService()
|
||||
|
||||
mock_docs = [
|
||||
{
|
||||
"law_name": "SchulG NRW",
|
||||
"paragraphs": [{"nr": "§ 42", "title": "Pflichten der Eltern"}],
|
||||
}
|
||||
]
|
||||
|
||||
with patch(
|
||||
"llm_gateway.services.communication_service.fetch_legal_references_from_db",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_fetch:
|
||||
mock_fetch.return_value = mock_docs
|
||||
|
||||
# Erster Aufruf
|
||||
await service.get_legal_references_async("NRW", "elternpflichten")
|
||||
|
||||
# Zweiter Aufruf sollte Cache nutzen
|
||||
await service.get_legal_references_async("NRW", "elternpflichten")
|
||||
|
||||
# fetch sollte nur einmal aufgerufen werden
|
||||
assert mock_fetch.call_count == 1
|
||||
|
||||
|
||||
class TestFetchLegalReferencesFromDB:
|
||||
"""Tests für fetch_legal_references_from_db Funktion."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_success(self):
|
||||
"""Test erfolgreicher API-Aufruf."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"documents": [
|
||||
{"law_name": "SchulG NRW", "paragraphs": []},
|
||||
]
|
||||
}
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
docs = await fetch_legal_references_from_db("NRW")
|
||||
|
||||
assert len(docs) == 1
|
||||
assert docs[0]["law_name"] == "SchulG NRW"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_api_error(self):
|
||||
"""Test API-Fehler gibt leere Liste zurück."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.return_value = mock_response
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
docs = await fetch_legal_references_from_db("NRW")
|
||||
|
||||
assert docs == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_network_error(self):
|
||||
"""Test Netzwerkfehler gibt leere Liste zurück."""
|
||||
import httpx
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_instance = AsyncMock()
|
||||
mock_instance.get.side_effect = httpx.ConnectError("Connection failed")
|
||||
mock_instance.__aenter__.return_value = mock_instance
|
||||
mock_instance.__aexit__.return_value = None
|
||||
mock_client.return_value = mock_instance
|
||||
|
||||
docs = await fetch_legal_references_from_db("NRW")
|
||||
|
||||
assert docs == []
|
||||
|
||||
|
||||
class TestParseDbReferencesToLegalRefs:
|
||||
"""Tests für parse_db_references_to_legal_refs Funktion."""
|
||||
|
||||
def test_parse_with_matching_paragraphs(self):
|
||||
"""Test Parsing mit passenden Paragraphen."""
|
||||
db_docs = [
|
||||
{
|
||||
"law_name": "SchulG NRW",
|
||||
"title": "Schulgesetz",
|
||||
"paragraphs": [
|
||||
{"nr": "§ 42", "title": "Pflichten der Eltern"},
|
||||
{"nr": "§ 1", "title": "Bildungsauftrag"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
refs = parse_db_references_to_legal_refs(db_docs, "elternpflichten")
|
||||
|
||||
assert len(refs) > 0
|
||||
# § 42 sollte für elternpflichten relevant sein
|
||||
assert any("42" in r.paragraph for r in refs)
|
||||
|
||||
def test_parse_without_paragraphs(self):
|
||||
"""Test Parsing ohne Paragraphen."""
|
||||
db_docs = [
|
||||
{
|
||||
"law_name": "SchulG NRW",
|
||||
"title": "Schulgesetz",
|
||||
"paragraphs": [],
|
||||
}
|
||||
]
|
||||
|
||||
refs = parse_db_references_to_legal_refs(db_docs, "elternpflichten")
|
||||
|
||||
# Sollte trotzdem Referenz erstellen
|
||||
assert len(refs) == 1
|
||||
assert refs[0].law == "SchulG NRW"
|
||||
|
||||
def test_parse_empty_docs(self):
|
||||
"""Test Parsing mit leerer Dokumentenliste."""
|
||||
refs = parse_db_references_to_legal_refs([], "elternpflichten")
|
||||
|
||||
assert refs == []
|
||||
|
||||
|
||||
class TestBuildSystemPrompt:
|
||||
"""Tests für build_system_prompt Methode."""
|
||||
|
||||
def test_build_system_prompt_contains_gfk(self):
|
||||
"""Test System-Prompt enthält GFK-Prinzipien."""
|
||||
service = CommunicationService()
|
||||
prompt = service.build_system_prompt(
|
||||
CommunicationType.BEHAVIOR,
|
||||
"NRW",
|
||||
CommunicationTone.PROFESSIONAL,
|
||||
)
|
||||
|
||||
# Prüfe Großbuchstaben-Varianten (wie im Prompt verwendet)
|
||||
assert "BEOBACHTUNG" in prompt
|
||||
assert "GEFÜHLE" in prompt
|
||||
assert "BEDÜRFNISSE" in prompt
|
||||
assert "BITTEN" in prompt
|
||||
|
||||
def test_build_system_prompt_contains_tone(self):
|
||||
"""Test System-Prompt enthält Tonalität."""
|
||||
service = CommunicationService()
|
||||
prompt = service.build_system_prompt(
|
||||
CommunicationType.BEHAVIOR,
|
||||
"NRW",
|
||||
CommunicationTone.WARM,
|
||||
)
|
||||
|
||||
assert "warmherzig" in prompt.lower()
|
||||
|
||||
|
||||
class TestBuildUserPrompt:
|
||||
"""Tests für build_user_prompt Methode."""
|
||||
|
||||
def test_build_user_prompt_with_context(self):
|
||||
"""Test User-Prompt mit Kontext."""
|
||||
service = CommunicationService()
|
||||
prompt = service.build_user_prompt(
|
||||
CommunicationType.BEHAVIOR,
|
||||
{
|
||||
"student_name": "Max",
|
||||
"parent_name": "Frau Müller",
|
||||
"situation": "Max stört häufig den Unterricht.",
|
||||
"additional_info": "Bereits 3x ermahnt.",
|
||||
},
|
||||
)
|
||||
|
||||
assert "Max" in prompt
|
||||
assert "Frau Müller" in prompt
|
||||
assert "stört" in prompt
|
||||
assert "ermahnt" in prompt
|
||||
|
||||
|
||||
class TestValidateCommunication:
|
||||
"""Tests für validate_communication Methode."""
|
||||
|
||||
def test_validate_good_communication(self):
|
||||
"""Test Validierung einer guten Kommunikation."""
|
||||
service = CommunicationService()
|
||||
text = """
|
||||
Sehr geehrte Frau Müller,
|
||||
|
||||
ich habe bemerkt, dass Max in letzter Zeit häufiger abwesend war.
|
||||
Ich möchte Sie gerne zu einem Gespräch einladen, da mir eine gute
|
||||
Zusammenarbeit sehr wichtig ist.
|
||||
|
||||
Wären Sie bereit, nächste Woche zu einem Termin zu kommen?
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
"""
|
||||
|
||||
result = service.validate_communication(text)
|
||||
|
||||
assert result["is_valid"] is True
|
||||
assert len(result["positive_elements"]) > 0
|
||||
assert result["gfk_score"] > 0.5
|
||||
|
||||
def test_validate_bad_communication(self):
|
||||
"""Test Validierung einer problematischen Kommunikation."""
|
||||
service = CommunicationService()
|
||||
text = """
|
||||
Sehr geehrte Frau Müller,
|
||||
|
||||
Sie müssen endlich etwas tun! Das Kind ist faul und respektlos.
|
||||
Sie sollten mehr kontrollieren.
|
||||
"""
|
||||
|
||||
result = service.validate_communication(text)
|
||||
|
||||
assert result["is_valid"] is False
|
||||
assert len(result["issues"]) > 0
|
||||
assert len(result["suggestions"]) > 0
|
||||
|
||||
|
||||
class TestGetAllCommunicationTypes:
|
||||
"""Tests für get_all_communication_types Methode."""
|
||||
|
||||
def test_returns_all_types(self):
|
||||
"""Test gibt alle Typen zurück."""
|
||||
service = CommunicationService()
|
||||
types = service.get_all_communication_types()
|
||||
|
||||
assert len(types) == 9
|
||||
assert all("value" in t and "label" in t for t in types)
|
||||
|
||||
|
||||
class TestGetAllTones:
|
||||
"""Tests für get_all_tones Methode."""
|
||||
|
||||
def test_returns_all_tones(self):
|
||||
"""Test gibt alle Tonalitäten zurück."""
|
||||
service = CommunicationService()
|
||||
tones = service.get_all_tones()
|
||||
|
||||
assert len(tones) == 5
|
||||
assert all("value" in t and "label" in t for t in tones)
|
||||
|
||||
|
||||
class TestGetStates:
|
||||
"""Tests für get_states Methode."""
|
||||
|
||||
def test_returns_all_16_bundeslaender(self):
|
||||
"""Test gibt alle 16 Bundesländer zurück."""
|
||||
service = CommunicationService()
|
||||
states = service.get_states()
|
||||
|
||||
assert len(states) == 16
|
||||
# Prüfe einige
|
||||
state_values = [s["value"] for s in states]
|
||||
assert "NRW" in state_values
|
||||
assert "BY" in state_values
|
||||
assert "BE" in state_values
|
||||
|
||||
|
||||
class TestGetCommunicationService:
|
||||
"""Tests für Singleton-Pattern."""
|
||||
|
||||
def test_singleton_pattern(self):
|
||||
"""Test dass get_communication_service immer dieselbe Instanz zurückgibt."""
|
||||
service1 = get_communication_service()
|
||||
service2 = get_communication_service()
|
||||
|
||||
assert service1 is service2
|
||||
|
||||
def test_returns_communication_service(self):
|
||||
"""Test dass CommunicationService zurückgegeben wird."""
|
||||
service = get_communication_service()
|
||||
|
||||
assert isinstance(service, CommunicationService)
|
||||
Reference in New Issue
Block a user