This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/tests/test_llm_gateway/test_communication_service.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

502 lines
16 KiB
Python

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