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:
594
backend/tests/test_alerts_agent/test_api_routes.py
Normal file
594
backend/tests/test_alerts_agent/test_api_routes.py
Normal file
@@ -0,0 +1,594 @@
|
||||
"""
|
||||
Tests für Alerts Agent API Routes.
|
||||
|
||||
Testet alle Endpoints: ingest, run, inbox, feedback, profile, stats.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from alerts_agent.api.routes import router, _alerts_store, _profile_store
|
||||
from alerts_agent.models.alert_item import AlertStatus
|
||||
|
||||
|
||||
# Test App erstellen
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
|
||||
class TestIngestEndpoint:
|
||||
"""Tests für POST /alerts/ingest."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def test_ingest_minimal(self):
|
||||
"""Test minimaler Alert-Import."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"title": "Test Alert",
|
||||
"url": "https://example.com/test",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "created"
|
||||
assert "id" in data
|
||||
assert len(data["id"]) == 36 # UUID
|
||||
|
||||
def test_ingest_full(self):
|
||||
"""Test vollständiger Alert-Import."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"title": "Neue Inklusions-Richtlinie",
|
||||
"url": "https://example.com/inklusion",
|
||||
"snippet": "Das Kultusministerium hat neue Richtlinien...",
|
||||
"topic_label": "Inklusion Bayern",
|
||||
"published_at": "2024-01-15T10:30:00",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "Inklusions-Richtlinie" in data["message"]
|
||||
|
||||
def test_ingest_stores_alert(self):
|
||||
"""Test dass Alert gespeichert wird."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"title": "Stored Alert",
|
||||
"url": "https://example.com/stored",
|
||||
},
|
||||
)
|
||||
|
||||
alert_id = response.json()["id"]
|
||||
assert alert_id in _alerts_store
|
||||
assert _alerts_store[alert_id].title == "Stored Alert"
|
||||
|
||||
def test_ingest_validation_missing_title(self):
|
||||
"""Test Validierung: Titel fehlt."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"url": "https://example.com/test",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_ingest_validation_missing_url(self):
|
||||
"""Test Validierung: URL fehlt."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"title": "Test",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_ingest_validation_empty_title(self):
|
||||
"""Test Validierung: Leerer Titel."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/ingest",
|
||||
json={
|
||||
"title": "",
|
||||
"url": "https://example.com",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
class TestRunEndpoint:
|
||||
"""Tests für POST /alerts/run."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def test_run_empty(self):
|
||||
"""Test Scoring ohne Alerts."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/run",
|
||||
json={"limit": 10},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["processed"] == 0
|
||||
assert data["keep"] == 0
|
||||
assert data["drop"] == 0
|
||||
|
||||
def test_run_scores_alerts(self):
|
||||
"""Test Scoring bewertet Alerts."""
|
||||
# Alerts importieren
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Inklusion in Schulen",
|
||||
"url": "https://example.com/1",
|
||||
})
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Stellenanzeige Lehrer",
|
||||
"url": "https://example.com/2",
|
||||
})
|
||||
|
||||
# Scoring starten
|
||||
response = self.client.post(
|
||||
"/api/alerts/run",
|
||||
json={"limit": 10},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["processed"] == 2
|
||||
assert data["keep"] + data["drop"] + data["review"] == 2
|
||||
|
||||
def test_run_keyword_scoring_keep(self):
|
||||
"""Test Keyword-Scoring: Priorität → KEEP."""
|
||||
# Explizit "Datenschutz Schule" als Snippet für besseren Match
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Neue Datenschutz-Regelung für Schulen",
|
||||
"url": "https://example.com/datenschutz",
|
||||
"snippet": "Datenschutz Schule DSGVO Regelung",
|
||||
})
|
||||
|
||||
response = self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
data = response.json()
|
||||
|
||||
# Sollte als KEEP oder REVIEW bewertet werden (nicht DROP)
|
||||
assert data["drop"] == 0
|
||||
assert data["keep"] + data["review"] == 1
|
||||
|
||||
def test_run_keyword_scoring_drop(self):
|
||||
"""Test Keyword-Scoring: Ausschluss → DROP."""
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Stellenanzeige: Schulleiter gesucht",
|
||||
"url": "https://example.com/job",
|
||||
})
|
||||
|
||||
response = self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
data = response.json()
|
||||
|
||||
assert data["drop"] == 1
|
||||
assert data["keep"] == 0
|
||||
|
||||
def test_run_skip_scored(self):
|
||||
"""Test bereits bewertete werden übersprungen."""
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Test Alert",
|
||||
"url": "https://example.com/test",
|
||||
})
|
||||
|
||||
# Erstes Scoring
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
# Zweites Scoring mit skip_scored=true
|
||||
response = self.client.post(
|
||||
"/api/alerts/run",
|
||||
json={"limit": 10, "skip_scored": True},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
assert data["processed"] == 0
|
||||
|
||||
def test_run_rescore(self):
|
||||
"""Test Re-Scoring mit skip_scored=false."""
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Test Alert",
|
||||
"url": "https://example.com/test",
|
||||
})
|
||||
|
||||
# Erstes Scoring
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
# Zweites Scoring mit skip_scored=false
|
||||
response = self.client.post(
|
||||
"/api/alerts/run",
|
||||
json={"limit": 10, "skip_scored": False},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
assert data["processed"] == 1
|
||||
|
||||
def test_run_limit(self):
|
||||
"""Test Limit Parameter."""
|
||||
# 5 Alerts importieren
|
||||
for i in range(5):
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": f"Alert {i}",
|
||||
"url": f"https://example.com/{i}",
|
||||
})
|
||||
|
||||
# Nur 2 scoren
|
||||
response = self.client.post(
|
||||
"/api/alerts/run",
|
||||
json={"limit": 2},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
assert data["processed"] == 2
|
||||
|
||||
def test_run_returns_duration(self):
|
||||
"""Test Duration wird zurückgegeben."""
|
||||
response = self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
data = response.json()
|
||||
|
||||
assert "duration_ms" in data
|
||||
assert isinstance(data["duration_ms"], int)
|
||||
|
||||
|
||||
class TestInboxEndpoint:
|
||||
"""Tests für GET /alerts/inbox."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def _create_and_score_alerts(self):
|
||||
"""Helfer: Erstelle und score Test-Alerts."""
|
||||
# KEEP Alert
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Inklusion Regelung",
|
||||
"url": "https://example.com/keep",
|
||||
})
|
||||
# DROP Alert
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Stellenanzeige",
|
||||
"url": "https://example.com/drop",
|
||||
})
|
||||
# Scoring
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
def test_inbox_empty(self):
|
||||
"""Test leere Inbox."""
|
||||
response = self.client.get("/api/alerts/inbox")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["items"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
def test_inbox_shows_keep_and_review(self):
|
||||
"""Test Inbox zeigt KEEP und REVIEW."""
|
||||
self._create_and_score_alerts()
|
||||
|
||||
response = self.client.get("/api/alerts/inbox")
|
||||
data = response.json()
|
||||
|
||||
# Nur KEEP sollte angezeigt werden (Stellenanzeige ist DROP)
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["relevance_decision"] == "KEEP"
|
||||
|
||||
def test_inbox_filter_by_decision(self):
|
||||
"""Test Inbox Filter nach Decision."""
|
||||
self._create_and_score_alerts()
|
||||
|
||||
# Nur DROP
|
||||
response = self.client.get("/api/alerts/inbox?decision=DROP")
|
||||
data = response.json()
|
||||
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["relevance_decision"] == "DROP"
|
||||
|
||||
def test_inbox_pagination(self):
|
||||
"""Test Inbox Pagination."""
|
||||
# 5 KEEP Alerts
|
||||
for i in range(5):
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": f"Inklusion Alert {i}",
|
||||
"url": f"https://example.com/{i}",
|
||||
})
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
# Erste Seite
|
||||
response = self.client.get("/api/alerts/inbox?page=1&page_size=2")
|
||||
data = response.json()
|
||||
|
||||
assert data["total"] == 5
|
||||
assert len(data["items"]) == 2
|
||||
assert data["page"] == 1
|
||||
assert data["page_size"] == 2
|
||||
|
||||
# Zweite Seite
|
||||
response = self.client.get("/api/alerts/inbox?page=2&page_size=2")
|
||||
data = response.json()
|
||||
|
||||
assert len(data["items"]) == 2
|
||||
|
||||
def test_inbox_item_fields(self):
|
||||
"""Test Inbox Items haben alle Felder."""
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Test Alert",
|
||||
"url": "https://example.com/test",
|
||||
"snippet": "Test snippet",
|
||||
"topic_label": "Test Topic",
|
||||
})
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
response = self.client.get("/api/alerts/inbox?decision=REVIEW")
|
||||
data = response.json()
|
||||
|
||||
if data["items"]:
|
||||
item = data["items"][0]
|
||||
assert "id" in item
|
||||
assert "title" in item
|
||||
assert "url" in item
|
||||
assert "snippet" in item
|
||||
assert "topic_label" in item
|
||||
assert "relevance_score" in item
|
||||
assert "relevance_decision" in item
|
||||
assert "status" in item
|
||||
|
||||
|
||||
class TestFeedbackEndpoint:
|
||||
"""Tests für POST /alerts/feedback."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def _create_alert(self):
|
||||
"""Helfer: Erstelle Test-Alert."""
|
||||
response = self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Test Alert",
|
||||
"url": "https://example.com/test",
|
||||
})
|
||||
return response.json()["id"]
|
||||
|
||||
def test_feedback_positive(self):
|
||||
"""Test positives Feedback."""
|
||||
alert_id = self._create_alert()
|
||||
|
||||
response = self.client.post(
|
||||
"/api/alerts/feedback",
|
||||
json={
|
||||
"alert_id": alert_id,
|
||||
"is_relevant": True,
|
||||
"reason": "Sehr relevant",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["profile_updated"] is True
|
||||
|
||||
def test_feedback_negative(self):
|
||||
"""Test negatives Feedback."""
|
||||
alert_id = self._create_alert()
|
||||
|
||||
response = self.client.post(
|
||||
"/api/alerts/feedback",
|
||||
json={
|
||||
"alert_id": alert_id,
|
||||
"is_relevant": False,
|
||||
"reason": "Werbung",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
|
||||
def test_feedback_updates_alert_status(self):
|
||||
"""Test Feedback aktualisiert Alert-Status."""
|
||||
alert_id = self._create_alert()
|
||||
|
||||
self.client.post("/api/alerts/feedback", json={
|
||||
"alert_id": alert_id,
|
||||
"is_relevant": True,
|
||||
})
|
||||
|
||||
assert _alerts_store[alert_id].status == AlertStatus.REVIEWED
|
||||
|
||||
def test_feedback_updates_profile(self):
|
||||
"""Test Feedback aktualisiert Profil."""
|
||||
alert_id = self._create_alert()
|
||||
|
||||
# Positives Feedback
|
||||
self.client.post("/api/alerts/feedback", json={
|
||||
"alert_id": alert_id,
|
||||
"is_relevant": True,
|
||||
"reason": "Wichtig",
|
||||
})
|
||||
|
||||
profile = _profile_store.get("default")
|
||||
assert profile is not None
|
||||
assert profile.total_kept == 1
|
||||
assert len(profile.positive_examples) == 1
|
||||
|
||||
def test_feedback_not_found(self):
|
||||
"""Test Feedback für nicht existierenden Alert."""
|
||||
response = self.client.post(
|
||||
"/api/alerts/feedback",
|
||||
json={
|
||||
"alert_id": "non-existent-id",
|
||||
"is_relevant": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_feedback_with_tags(self):
|
||||
"""Test Feedback mit Tags."""
|
||||
alert_id = self._create_alert()
|
||||
|
||||
response = self.client.post(
|
||||
"/api/alerts/feedback",
|
||||
json={
|
||||
"alert_id": alert_id,
|
||||
"is_relevant": True,
|
||||
"tags": ["wichtig", "inklusion"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestProfileEndpoint:
|
||||
"""Tests für GET/PUT /alerts/profile."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def test_get_profile_default(self):
|
||||
"""Test Default-Profil abrufen."""
|
||||
response = self.client.get("/api/alerts/profile")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert "priorities" in data
|
||||
assert "exclusions" in data
|
||||
assert len(data["priorities"]) > 0 # Default hat Prioritäten
|
||||
|
||||
def test_get_profile_creates_default(self):
|
||||
"""Test Profil wird automatisch erstellt."""
|
||||
assert "default" not in _profile_store
|
||||
|
||||
self.client.get("/api/alerts/profile")
|
||||
|
||||
assert "default" in _profile_store
|
||||
|
||||
def test_update_profile_priorities(self):
|
||||
"""Test Prioritäten aktualisieren."""
|
||||
response = self.client.put(
|
||||
"/api/alerts/profile",
|
||||
json={
|
||||
"priorities": [
|
||||
{"label": "Neue Priorität", "weight": 0.9},
|
||||
{"label": "Zweite Priorität", "weight": 0.7},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["priorities"]) == 2
|
||||
assert data["priorities"][0]["label"] == "Neue Priorität"
|
||||
|
||||
def test_update_profile_exclusions(self):
|
||||
"""Test Ausschlüsse aktualisieren."""
|
||||
response = self.client.put(
|
||||
"/api/alerts/profile",
|
||||
json={
|
||||
"exclusions": ["Spam", "Werbung", "Newsletter"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "Spam" in data["exclusions"]
|
||||
assert len(data["exclusions"]) == 3
|
||||
|
||||
def test_update_profile_policies(self):
|
||||
"""Test Policies aktualisieren."""
|
||||
response = self.client.put(
|
||||
"/api/alerts/profile",
|
||||
json={
|
||||
"policies": {
|
||||
"max_age_days": 14,
|
||||
"prefer_german_sources": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["policies"]["max_age_days"] == 14
|
||||
|
||||
def test_profile_stats(self):
|
||||
"""Test Profil enthält Statistiken."""
|
||||
response = self.client.get("/api/alerts/profile")
|
||||
data = response.json()
|
||||
|
||||
assert "total_scored" in data
|
||||
assert "total_kept" in data
|
||||
assert "total_dropped" in data
|
||||
|
||||
|
||||
class TestStatsEndpoint:
|
||||
"""Tests für GET /alerts/stats."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup für jeden Test."""
|
||||
_alerts_store.clear()
|
||||
_profile_store.clear()
|
||||
self.client = TestClient(app)
|
||||
|
||||
def test_stats_empty(self):
|
||||
"""Test Stats ohne Alerts."""
|
||||
response = self.client.get("/api/alerts/stats")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total_alerts"] == 0
|
||||
|
||||
def test_stats_with_alerts(self):
|
||||
"""Test Stats mit Alerts."""
|
||||
# Alerts erstellen und scoren
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Inklusion",
|
||||
"url": "https://example.com/1",
|
||||
})
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Stellenanzeige",
|
||||
"url": "https://example.com/2",
|
||||
})
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
response = self.client.get("/api/alerts/stats")
|
||||
data = response.json()
|
||||
|
||||
assert data["total_alerts"] == 2
|
||||
assert "by_status" in data
|
||||
assert "by_decision" in data
|
||||
assert "scored" in data["by_status"]
|
||||
|
||||
def test_stats_avg_score(self):
|
||||
"""Test Durchschnittlicher Score."""
|
||||
self.client.post("/api/alerts/ingest", json={
|
||||
"title": "Test",
|
||||
"url": "https://example.com/1",
|
||||
})
|
||||
self.client.post("/api/alerts/run", json={"limit": 10})
|
||||
|
||||
response = self.client.get("/api/alerts/stats")
|
||||
data = response.json()
|
||||
|
||||
assert "avg_score" in data
|
||||
assert data["avg_score"] is not None
|
||||
Reference in New Issue
Block a user