""" Tests für den Feedback-Learning-Mechanismus. Testet wie das System aus Nutzer-Feedback lernt und das Profil anpasst. """ import pytest from datetime import datetime from alerts_agent.models.relevance_profile import RelevanceProfile, PriorityItem from alerts_agent.models.alert_item import AlertItem class TestFeedbackLearning: """Tests für den Feedback-Learning-Mechanismus.""" def test_positive_feedback_adds_example(self): """Test positives Feedback fügt Beispiel hinzu.""" profile = RelevanceProfile() profile.update_from_feedback( alert_title="Wichtiger Artikel zur Inklusion", alert_url="https://example.com/inklusion", is_relevant=True, reason="Sehr relevant für meine Arbeit", ) assert len(profile.positive_examples) == 1 assert profile.positive_examples[0]["title"] == "Wichtiger Artikel zur Inklusion" assert profile.positive_examples[0]["reason"] == "Sehr relevant für meine Arbeit" def test_negative_feedback_adds_example(self): """Test negatives Feedback fügt Beispiel hinzu.""" profile = RelevanceProfile() profile.update_from_feedback( alert_title="Stellenanzeige Lehrer", alert_url="https://example.com/job", is_relevant=False, reason="Nur Werbung", ) assert len(profile.negative_examples) == 1 assert profile.negative_examples[0]["title"] == "Stellenanzeige Lehrer" def test_feedback_updates_counters(self): """Test Feedback aktualisiert Zähler.""" profile = RelevanceProfile() # 3 positive, 2 negative for i in range(3): profile.update_from_feedback(f"Good {i}", f"url{i}", True) for i in range(2): profile.update_from_feedback(f"Bad {i}", f"url{i}", False) assert profile.total_scored == 5 assert profile.total_kept == 3 assert profile.total_dropped == 2 def test_examples_limited_to_20(self): """Test Beispiele werden auf 20 begrenzt.""" profile = RelevanceProfile() # 25 Beispiele hinzufügen for i in range(25): profile.update_from_feedback( f"Example {i}", f"https://example.com/{i}", is_relevant=True, ) assert len(profile.positive_examples) == 20 # Die neuesten sollten behalten werden titles = [ex["title"] for ex in profile.positive_examples] assert "Example 24" in titles assert "Example 0" not in titles # Ältestes sollte weg sein def test_examples_in_prompt_context(self): """Test Beispiele erscheinen im Prompt-Kontext.""" profile = RelevanceProfile() profile.update_from_feedback( "Relevanter Artikel", "https://example.com/good", is_relevant=True, reason="Wichtig", ) profile.update_from_feedback( "Irrelevanter Artikel", "https://example.com/bad", is_relevant=False, reason="Spam", ) context = profile.get_prompt_context() assert "Relevanter Artikel" in context assert "Irrelevanter Artikel" in context assert "relevante Alerts" in context assert "irrelevante Alerts" in context class TestProfileEvolution: """Tests für die Evolution des Profils über Zeit.""" def test_profile_learns_from_feedback_pattern(self): """Test Profil lernt aus Feedback-Mustern.""" profile = RelevanceProfile() # Simuliere Feedback-Muster: Inklusions-Artikel sind relevant inklusion_articles = [ ("Neue Inklusions-Verordnung", "https://example.com/1"), ("Inklusion in Bayern verstärkt", "https://example.com/2"), ("Förderbedarf: Neue Richtlinien", "https://example.com/3"), ] for title, url in inklusion_articles: profile.update_from_feedback(title, url, is_relevant=True, reason="Inklusion") # Simuliere irrelevante Artikel spam_articles = [ ("Newsletter Dezember", "https://example.com/spam1"), ("Pressemitteilung", "https://example.com/spam2"), ] for title, url in spam_articles: profile.update_from_feedback(title, url, is_relevant=False, reason="Spam") # Prompt-Kontext sollte die Muster reflektieren context = profile.get_prompt_context() # Alle positiven Beispiele sollten Inklusions-bezogen sein for title, _ in inklusion_articles: assert title in context # Negative Beispiele sollten auch vorhanden sein for title, _ in spam_articles: assert title in context def test_profile_statistics_reflect_decisions(self): """Test Profil-Statistiken reflektieren Entscheidungen.""" profile = RelevanceProfile() # 70% relevant, 30% irrelevant for i in range(70): profile.update_from_feedback(f"Good {i}", f"url{i}", True) for i in range(30): profile.update_from_feedback(f"Bad {i}", f"url{i}", False) assert profile.total_scored == 100 assert profile.total_kept == 70 assert profile.total_dropped == 30 # Keep-Rate sollte 70% sein keep_rate = profile.total_kept / profile.total_scored assert keep_rate == 0.7 class TestFeedbackWithPriorities: """Tests für Feedback in Kombination mit Prioritäten.""" def test_priority_keywords_in_feedback(self): """Test Feedback-Beispiele ergänzen Prioritäts-Keywords.""" profile = RelevanceProfile() profile.add_priority( "Inklusion", weight=0.9, keywords=["Förderbedarf", "inklusiv"], ) # Feedback mit zusätzlichem Kontext profile.update_from_feedback( "Nachteilsausgleich für Schüler mit Förderbedarf", "https://example.com/nachteilsausgleich", is_relevant=True, reason="Nachteilsausgleich ist wichtig für Inklusion", ) # Das Feedback-Beispiel sollte im Kontext erscheinen context = profile.get_prompt_context() assert "Nachteilsausgleich" in context def test_exclusion_patterns_from_feedback(self): """Test Ausschlüsse werden durch Feedback-Muster erkannt.""" profile = RelevanceProfile() # Mehrere Stellenanzeigen als irrelevant markieren for i in range(5): profile.update_from_feedback( f"Stellenanzeige: Position {i}", f"https://example.com/job{i}", is_relevant=False, reason="Stellenanzeige", ) # Das Muster sollte in negativen Beispielen sichtbar sein assert len(profile.negative_examples) == 5 assert all("Stellenanzeige" in ex["title"] for ex in profile.negative_examples) class TestDefaultProfileFeedback: """Tests für Feedback mit dem Default-Bildungsprofil.""" def test_default_profile_with_feedback(self): """Test Default-Profil kann Feedback verarbeiten.""" profile = RelevanceProfile.create_default_education_profile() # Starte mit Default-Werten initial_examples = len(profile.positive_examples) # Füge Feedback hinzu profile.update_from_feedback( "Datenschutz an Schulen: Neue DSGVO-Richtlinien", "https://example.com/dsgvo", is_relevant=True, reason="DSGVO-relevant", ) assert len(profile.positive_examples) == initial_examples + 1 assert profile.total_kept == 1 def test_default_priorities_preserved_after_feedback(self): """Test Default-Prioritäten bleiben nach Feedback erhalten.""" profile = RelevanceProfile.create_default_education_profile() original_priorities = len(profile.priorities) # Feedback sollte Prioritäten nicht ändern profile.update_from_feedback("Test", "https://test.com", True) assert len(profile.priorities) == original_priorities class TestFeedbackTimestamps: """Tests für Feedback-Zeitstempel.""" def test_feedback_has_timestamp(self): """Test Feedback-Beispiele haben Zeitstempel.""" profile = RelevanceProfile() profile.update_from_feedback( "Test Article", "https://example.com", is_relevant=True, ) example = profile.positive_examples[0] assert "added_at" in example # Sollte ein ISO-Format Datum sein datetime.fromisoformat(example["added_at"]) def test_profile_updated_at_changes(self): """Test updated_at ändert sich nach Feedback.""" profile = RelevanceProfile() original_updated = profile.updated_at # Kurz warten und Feedback geben import time time.sleep(0.01) profile.update_from_feedback("Test", "https://test.com", True) assert profile.updated_at > original_updated