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_alerts_repository.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

467 lines
16 KiB
Python

"""
Tests für Alerts Agent Repository.
Testet CRUD-Operationen für Topics, Items, Rules und Profiles.
"""
import pytest
from datetime import datetime
from unittest.mock import MagicMock, patch
# Test mit In-Memory SQLite für Isolation
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from alerts_agent.db.models import (
AlertTopicDB, AlertItemDB, AlertRuleDB, AlertProfileDB,
AlertSourceEnum, AlertStatusEnum, RelevanceDecisionEnum,
FeedTypeEnum, RuleActionEnum
)
from alerts_agent.db.repository import (
TopicRepository, AlertItemRepository, RuleRepository, ProfileRepository
)
# Nutze classroom_engine Base für konsistente Schemas
from classroom_engine.database import Base
@pytest.fixture
def db_session():
"""Erstellt eine In-Memory SQLite-Session für Tests."""
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
yield session
session.close()
# =============================================================================
# TOPIC REPOSITORY TESTS
# =============================================================================
class TestTopicRepository:
"""Tests für TopicRepository."""
def test_create_topic(self, db_session):
"""Test: Topic erstellen."""
repo = TopicRepository(db_session)
topic = repo.create(
name="Test Topic",
feed_url="https://example.com/feed",
feed_type="rss",
description="Test Description",
)
assert topic.id is not None
assert topic.name == "Test Topic"
assert topic.feed_url == "https://example.com/feed"
assert topic.feed_type == FeedTypeEnum.RSS
assert topic.is_active is True
def test_get_topic_by_id(self, db_session):
"""Test: Topic nach ID abrufen."""
repo = TopicRepository(db_session)
created = repo.create(name="Find Me")
found = repo.get_by_id(created.id)
assert found is not None
assert found.name == "Find Me"
def test_get_topic_not_found(self, db_session):
"""Test: Topic nicht gefunden."""
repo = TopicRepository(db_session)
found = repo.get_by_id("nonexistent-id")
assert found is None
def test_update_topic(self, db_session):
"""Test: Topic aktualisieren."""
repo = TopicRepository(db_session)
topic = repo.create(name="Original Name")
updated = repo.update(topic.id, name="Updated Name", is_active=False)
assert updated.name == "Updated Name"
assert updated.is_active is False
def test_delete_topic(self, db_session):
"""Test: Topic löschen."""
repo = TopicRepository(db_session)
topic = repo.create(name="To Delete")
result = repo.delete(topic.id)
assert result is True
assert repo.get_by_id(topic.id) is None
def test_get_all_topics(self, db_session):
"""Test: Alle Topics abrufen."""
repo = TopicRepository(db_session)
repo.create(name="Topic 1")
repo.create(name="Topic 2")
repo.create(name="Topic 3")
topics = repo.get_all()
assert len(topics) == 3
def test_get_active_topics(self, db_session):
"""Test: Nur aktive Topics abrufen."""
repo = TopicRepository(db_session)
repo.create(name="Active 1")
repo.create(name="Active 2")
inactive = repo.create(name="Inactive")
repo.update(inactive.id, is_active=False)
active_topics = repo.get_all(is_active=True)
assert len(active_topics) == 2
# =============================================================================
# ALERT ITEM REPOSITORY TESTS
# =============================================================================
class TestAlertItemRepository:
"""Tests für AlertItemRepository."""
@pytest.fixture
def topic_id(self, db_session):
"""Erstellt ein Test-Topic und gibt die ID zurück."""
topic_repo = TopicRepository(db_session)
topic = topic_repo.create(name="Test Topic")
return topic.id
def test_create_alert(self, db_session, topic_id):
"""Test: Alert erstellen."""
repo = AlertItemRepository(db_session)
alert = repo.create(
topic_id=topic_id,
title="Test Alert",
url="https://example.com/article",
snippet="Test snippet content",
)
assert alert.id is not None
assert alert.title == "Test Alert"
assert alert.url_hash is not None
assert alert.status == AlertStatusEnum.NEW
def test_create_if_not_exists_creates(self, db_session, topic_id):
"""Test: Alert erstellen wenn nicht existiert."""
repo = AlertItemRepository(db_session)
alert = repo.create_if_not_exists(
topic_id=topic_id,
title="New Alert",
url="https://example.com/new",
)
assert alert is not None
assert alert.title == "New Alert"
def test_create_if_not_exists_duplicate(self, db_session, topic_id):
"""Test: Duplikat wird nicht erstellt."""
repo = AlertItemRepository(db_session)
url = "https://example.com/duplicate"
first = repo.create_if_not_exists(topic_id=topic_id, title="First", url=url)
second = repo.create_if_not_exists(topic_id=topic_id, title="Second", url=url)
assert first is not None
assert second is None # Duplikat
def test_update_scoring(self, db_session, topic_id):
"""Test: Scoring aktualisieren."""
repo = AlertItemRepository(db_session)
alert = repo.create(topic_id=topic_id, title="To Score", url="https://example.com/score")
updated = repo.update_scoring(
alert_id=alert.id,
score=0.85,
decision="KEEP",
reasons=["relevant"],
summary="Important article",
model="test-model",
)
assert updated.relevance_score == 0.85
assert updated.relevance_decision == RelevanceDecisionEnum.KEEP
assert updated.status == AlertStatusEnum.SCORED
def test_get_inbox(self, db_session, topic_id):
"""Test: Inbox abrufen."""
repo = AlertItemRepository(db_session)
# Erstelle Alerts mit verschiedenen Decisions
alert1 = repo.create(topic_id=topic_id, title="Keep Alert", url="https://example.com/1")
repo.update_scoring(alert1.id, 0.9, "KEEP", [], None, None)
alert2 = repo.create(topic_id=topic_id, title="Drop Alert", url="https://example.com/2")
repo.update_scoring(alert2.id, 0.1, "DROP", [], None, None)
alert3 = repo.create(topic_id=topic_id, title="Review Alert", url="https://example.com/3")
repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, None)
# Default-Inbox (KEEP + REVIEW)
inbox = repo.get_inbox()
assert len(inbox) == 2 # KEEP und REVIEW
# Nur KEEP
keep_only = repo.get_inbox(decision="KEEP")
assert len(keep_only) == 1
def test_get_unscored(self, db_session, topic_id):
"""Test: Unbewertete Alerts abrufen."""
repo = AlertItemRepository(db_session)
# Erstelle neue Alerts
repo.create(topic_id=topic_id, title="Unscored 1", url="https://example.com/u1")
repo.create(topic_id=topic_id, title="Unscored 2", url="https://example.com/u2")
# Einen bewerten
alert3 = repo.create(topic_id=topic_id, title="Scored", url="https://example.com/s1")
repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, None)
unscored = repo.get_unscored()
assert len(unscored) == 2
def test_mark_reviewed(self, db_session, topic_id):
"""Test: Alert als reviewed markieren."""
repo = AlertItemRepository(db_session)
alert = repo.create(topic_id=topic_id, title="To Review", url="https://example.com/review")
reviewed = repo.mark_reviewed(
alert_id=alert.id,
is_relevant=True,
notes="Good article",
tags=["important"],
)
assert reviewed.status == AlertStatusEnum.REVIEWED
assert reviewed.user_marked_relevant is True
assert reviewed.user_notes == "Good article"
assert "important" in reviewed.user_tags
# =============================================================================
# RULE REPOSITORY TESTS
# =============================================================================
class TestRuleRepository:
"""Tests für RuleRepository."""
def test_create_rule(self, db_session):
"""Test: Regel erstellen."""
repo = RuleRepository(db_session)
rule = repo.create(
name="Test Rule",
conditions=[{"field": "title", "op": "contains", "value": "test"}],
action_type="keep",
priority=10,
)
assert rule.id is not None
assert rule.name == "Test Rule"
assert rule.priority == 10
assert rule.is_active is True
def test_get_active_rules_ordered(self, db_session):
"""Test: Aktive Regeln nach Priorität sortiert."""
repo = RuleRepository(db_session)
repo.create(name="Low Priority", conditions=[], priority=1)
repo.create(name="High Priority", conditions=[], priority=100)
repo.create(name="Medium Priority", conditions=[], priority=50)
rules = repo.get_active()
assert len(rules) == 3
assert rules[0].name == "High Priority"
assert rules[1].name == "Medium Priority"
assert rules[2].name == "Low Priority"
def test_update_rule(self, db_session):
"""Test: Regel aktualisieren."""
repo = RuleRepository(db_session)
rule = repo.create(name="Original", conditions=[], action_type="keep")
updated = repo.update(rule.id, name="Updated", action_type="drop")
assert updated.name == "Updated"
assert updated.action_type == RuleActionEnum.DROP
def test_increment_match_count(self, db_session):
"""Test: Match-Counter erhöhen."""
repo = RuleRepository(db_session)
rule = repo.create(name="Matcher", conditions=[])
assert rule.match_count == 0
repo.increment_match_count(rule.id)
repo.increment_match_count(rule.id)
updated = repo.get_by_id(rule.id)
assert updated.match_count == 2
assert updated.last_matched_at is not None
# =============================================================================
# PROFILE REPOSITORY TESTS
# =============================================================================
class TestProfileRepository:
"""Tests für ProfileRepository."""
def test_create_default_profile(self, db_session):
"""Test: Default-Profil erstellen."""
repo = ProfileRepository(db_session)
profile = repo.create_default_education_profile()
assert profile.id is not None
assert len(profile.priorities) > 0
assert "Inklusion" in [p["label"] for p in profile.priorities]
def test_get_or_create(self, db_session):
"""Test: Get-or-Create Pattern."""
repo = ProfileRepository(db_session)
# Erstes Mal erstellt
profile1 = repo.get_or_create(user_id="user-123")
assert profile1 is not None
# Zweites Mal holt existierendes
profile2 = repo.get_or_create(user_id="user-123")
assert profile2.id == profile1.id
def test_update_priorities(self, db_session):
"""Test: Prioritäten aktualisieren."""
repo = ProfileRepository(db_session)
profile = repo.get_or_create()
new_priorities = [
{"label": "New Priority", "weight": 0.9, "keywords": ["test"]}
]
updated = repo.update_priorities(profile.id, new_priorities)
assert len(updated.priorities) == 1
assert updated.priorities[0]["label"] == "New Priority"
def test_add_feedback_positive(self, db_session):
"""Test: Positives Feedback hinzufügen."""
repo = ProfileRepository(db_session)
profile = repo.get_or_create()
initial_kept = profile.total_kept
repo.add_feedback(
profile_id=profile.id,
title="Relevant Article",
url="https://example.com/relevant",
is_relevant=True,
reason="Very informative",
)
updated = repo.get_by_id(profile.id)
assert updated.total_kept == initial_kept + 1
assert len(updated.positive_examples) == 1
def test_add_feedback_negative(self, db_session):
"""Test: Negatives Feedback hinzufügen."""
repo = ProfileRepository(db_session)
profile = repo.get_or_create()
initial_dropped = profile.total_dropped
repo.add_feedback(
profile_id=profile.id,
title="Irrelevant Article",
url="https://example.com/irrelevant",
is_relevant=False,
reason="Off-topic",
)
updated = repo.get_by_id(profile.id)
assert updated.total_dropped == initial_dropped + 1
assert len(updated.negative_examples) == 1
def test_feedback_limits_examples(self, db_session):
"""Test: Beispiele werden auf 20 begrenzt."""
repo = ProfileRepository(db_session)
profile = repo.get_or_create()
# Füge 25 positive Beispiele hinzu
for i in range(25):
repo.add_feedback(
profile_id=profile.id,
title=f"Article {i}",
url=f"https://example.com/{i}",
is_relevant=True,
)
updated = repo.get_by_id(profile.id)
assert len(updated.positive_examples) == 20 # Max 20
# =============================================================================
# INTEGRATION TESTS
# =============================================================================
class TestRepositoryIntegration:
"""Integration Tests für Repository-Zusammenspiel."""
def test_topic_with_alerts_cascade_delete(self, db_session):
"""Test: Topic-Löschung löscht auch zugehörige Alerts."""
topic_repo = TopicRepository(db_session)
alert_repo = AlertItemRepository(db_session)
# Erstelle Topic mit Alerts
topic = topic_repo.create(name="To Delete")
alert_repo.create(topic_id=topic.id, title="Alert 1", url="https://example.com/1")
alert_repo.create(topic_id=topic.id, title="Alert 2", url="https://example.com/2")
# Prüfe dass Alerts existieren
alerts = alert_repo.get_by_topic(topic.id)
assert len(alerts) == 2
# Lösche Topic
topic_repo.delete(topic.id)
# Alerts sollten auch gelöscht sein (CASCADE)
alerts_after = alert_repo.get_by_topic(topic.id)
assert len(alerts_after) == 0
def test_scoring_workflow(self, db_session):
"""Test: Kompletter Scoring-Workflow."""
topic_repo = TopicRepository(db_session)
alert_repo = AlertItemRepository(db_session)
profile_repo = ProfileRepository(db_session)
# Setup
topic = topic_repo.create(name="Workflow Test")
profile = profile_repo.create_default_education_profile()
# Alerts erstellen
alert1 = alert_repo.create(topic_id=topic.id, title="Inklusion im Unterricht", url="https://example.com/a1")
alert2 = alert_repo.create(topic_id=topic.id, title="Stellenanzeige Lehrer", url="https://example.com/a2")
alert3 = alert_repo.create(topic_id=topic.id, title="Neutral News", url="https://example.com/a3")
# Scoring simulieren
alert_repo.update_scoring(alert1.id, 0.85, "KEEP", ["priority_match"], "Relevant", "test")
alert_repo.update_scoring(alert2.id, 0.1, "DROP", ["exclusion_match"], None, "test")
alert_repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, "test")
# Stats prüfen
by_decision = alert_repo.count_by_decision(topic.id)
assert by_decision.get("KEEP", 0) == 1
assert by_decision.get("DROP", 0) == 1
assert by_decision.get("REVIEW", 0) == 1