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:
346
backend/tests/test_klausur_korrektur_api.py
Normal file
346
backend/tests/test_klausur_korrektur_api.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Tests fuer die Klausur-Korrektur API
|
||||
|
||||
Tests fuer:
|
||||
- Klausuren erstellen, abrufen, aktualisieren, loeschen
|
||||
- Text-Quellen hinzufuegen und verwalten
|
||||
- Schuelerarbeiten hochladen
|
||||
- Bewertung und Gutachten
|
||||
- 15-Punkte-Notensystem
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
# Import des zu testenden Moduls
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from klausur_korrektur_api import (
|
||||
router,
|
||||
AbiturKlausur,
|
||||
KlausurModus,
|
||||
KlausurStatus,
|
||||
TextSource,
|
||||
TextSourceType,
|
||||
TextSourceStatus,
|
||||
StudentKlausur,
|
||||
StudentKlausurStatus,
|
||||
Erwartungshorizont,
|
||||
Aufgabe,
|
||||
CriterionScore,
|
||||
Gutachten,
|
||||
ExaminerResult,
|
||||
DEFAULT_CRITERIA,
|
||||
GRADE_THRESHOLDS,
|
||||
calculate_15_point_grade,
|
||||
klausuren_db,
|
||||
)
|
||||
|
||||
|
||||
class TestGradeCalculation:
|
||||
"""Tests fuer die Notenberechnung im 15-Punkte-System."""
|
||||
|
||||
def test_calculate_15_point_grade_perfect(self):
|
||||
"""100% sollte 15 Punkte ergeben."""
|
||||
assert calculate_15_point_grade(100.0) == 15
|
||||
|
||||
def test_calculate_15_point_grade_95(self):
|
||||
"""95% sollte 15 Punkte ergeben (1+)."""
|
||||
assert calculate_15_point_grade(95.0) == 15
|
||||
|
||||
def test_calculate_15_point_grade_90(self):
|
||||
"""90% sollte 14 Punkte ergeben (1)."""
|
||||
assert calculate_15_point_grade(90.0) == 14
|
||||
|
||||
def test_calculate_15_point_grade_85(self):
|
||||
"""85% sollte 13 Punkte ergeben (1-)."""
|
||||
assert calculate_15_point_grade(85.0) == 13
|
||||
|
||||
def test_calculate_15_point_grade_50(self):
|
||||
"""50% sollte 6 Punkte ergeben (4+)."""
|
||||
assert calculate_15_point_grade(50.0) == 6
|
||||
|
||||
def test_calculate_15_point_grade_45(self):
|
||||
"""45% sollte 5 Punkte ergeben (4)."""
|
||||
assert calculate_15_point_grade(45.0) == 5
|
||||
|
||||
def test_calculate_15_point_grade_below_threshold(self):
|
||||
"""19% sollte 0 Punkte ergeben (6)."""
|
||||
assert calculate_15_point_grade(19.0) == 0
|
||||
|
||||
def test_calculate_15_point_grade_zero(self):
|
||||
"""0% sollte 0 Punkte ergeben."""
|
||||
assert calculate_15_point_grade(0.0) == 0
|
||||
|
||||
def test_calculate_15_point_grade_boundary_values(self):
|
||||
"""Test aller Grenzwerte."""
|
||||
expected_results = [
|
||||
(95, 15),
|
||||
(94.9, 14),
|
||||
(90, 14),
|
||||
(89.9, 13),
|
||||
(85, 13),
|
||||
(84.9, 12),
|
||||
(80, 12),
|
||||
(79.9, 11),
|
||||
(75, 11),
|
||||
(74.9, 10),
|
||||
(70, 10),
|
||||
(69.9, 9),
|
||||
(65, 9),
|
||||
(64.9, 8),
|
||||
(60, 8),
|
||||
(59.9, 7),
|
||||
(55, 7),
|
||||
(54.9, 6),
|
||||
(50, 6),
|
||||
(49.9, 5),
|
||||
(45, 5),
|
||||
(44.9, 4),
|
||||
(40, 4),
|
||||
(39.9, 3),
|
||||
(33, 3),
|
||||
(32.9, 2),
|
||||
(27, 2),
|
||||
(26.9, 1),
|
||||
(20, 1),
|
||||
(19.9, 0),
|
||||
]
|
||||
for percentage, expected_points in expected_results:
|
||||
result = calculate_15_point_grade(percentage)
|
||||
assert result == expected_points, f"Expected {expected_points} for {percentage}%, got {result}"
|
||||
|
||||
|
||||
class TestGradeThresholds:
|
||||
"""Tests fuer die Notenschwellen."""
|
||||
|
||||
def test_all_thresholds_present(self):
|
||||
"""Alle 16 Notenpunkte (0-15) sollten definiert sein."""
|
||||
assert len(GRADE_THRESHOLDS) == 16
|
||||
for i in range(16):
|
||||
assert i in GRADE_THRESHOLDS
|
||||
|
||||
def test_thresholds_descending(self):
|
||||
"""Schwellen sollten von 15 nach 0 absteigend sein."""
|
||||
prev_threshold = 100
|
||||
for points in range(15, -1, -1):
|
||||
threshold = GRADE_THRESHOLDS[points]
|
||||
assert threshold < prev_threshold or (points == 15 and threshold <= prev_threshold)
|
||||
prev_threshold = threshold
|
||||
|
||||
|
||||
class TestDefaultCriteria:
|
||||
"""Tests fuer die Standard-Bewertungskriterien."""
|
||||
|
||||
def test_criteria_weights_sum_to_one(self):
|
||||
"""Gewichte aller Kriterien sollten 1.0 ergeben."""
|
||||
total_weight = sum(c["weight"] for c in DEFAULT_CRITERIA.values())
|
||||
assert abs(total_weight - 1.0) < 0.001
|
||||
|
||||
def test_required_criteria_present(self):
|
||||
"""Alle erforderlichen Kriterien sollten vorhanden sein."""
|
||||
required = ["rechtschreibung", "grammatik", "inhalt", "struktur", "stil"]
|
||||
for criterion in required:
|
||||
assert criterion in DEFAULT_CRITERIA
|
||||
|
||||
def test_inhalt_has_highest_weight(self):
|
||||
"""Inhalt sollte das hoechste Gewicht haben."""
|
||||
inhalt_weight = DEFAULT_CRITERIA["inhalt"]["weight"]
|
||||
for name, criterion in DEFAULT_CRITERIA.items():
|
||||
if name != "inhalt":
|
||||
assert criterion["weight"] <= inhalt_weight
|
||||
|
||||
|
||||
class TestKlausurModels:
|
||||
"""Tests fuer die Datenmodelle."""
|
||||
|
||||
def test_create_abitur_klausur(self):
|
||||
"""Eine neue Klausur sollte erstellt werden koennen."""
|
||||
now = datetime.now()
|
||||
klausur = AbiturKlausur(
|
||||
id="test-123",
|
||||
title="Deutsch LK Q4",
|
||||
subject="deutsch",
|
||||
modus=KlausurModus.LANDES_ABITUR,
|
||||
year=2025,
|
||||
semester="Q4",
|
||||
kurs="LK",
|
||||
class_id=None,
|
||||
status=KlausurStatus.DRAFT,
|
||||
text_sources=[],
|
||||
erwartungshorizont=None,
|
||||
students=[],
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
assert klausur.id == "test-123"
|
||||
assert klausur.modus == KlausurModus.LANDES_ABITUR
|
||||
|
||||
def test_create_text_source(self):
|
||||
"""Eine Textquelle sollte erstellt werden koennen."""
|
||||
source = TextSource(
|
||||
id="src-1",
|
||||
source_type=TextSourceType.NIBIS,
|
||||
title="Kafka - Die Verwandlung",
|
||||
author="Franz Kafka",
|
||||
content="Als Gregor Samsa eines Morgens...",
|
||||
nibis_id=None,
|
||||
license_status=TextSourceStatus.VERIFIED,
|
||||
license_info={"license": "PD"},
|
||||
created_at=datetime.now()
|
||||
)
|
||||
assert source.license_status == TextSourceStatus.VERIFIED
|
||||
|
||||
def test_student_klausur_status_workflow(self):
|
||||
"""Der Status-Workflow einer Schuelerarbeit sollte korrekt sein."""
|
||||
statuses = list(StudentKlausurStatus)
|
||||
expected_order = [
|
||||
StudentKlausurStatus.UPLOADED,
|
||||
StudentKlausurStatus.OCR_PROCESSING,
|
||||
StudentKlausurStatus.OCR_COMPLETE,
|
||||
StudentKlausurStatus.ANALYZING,
|
||||
StudentKlausurStatus.FIRST_EXAMINER,
|
||||
StudentKlausurStatus.SECOND_EXAMINER,
|
||||
StudentKlausurStatus.COMPLETED,
|
||||
StudentKlausurStatus.ERROR, # Error state can occur at any point
|
||||
]
|
||||
assert statuses == expected_order
|
||||
|
||||
|
||||
class TestCriterionScore:
|
||||
"""Tests fuer die Bewertungskriterien-Punkte."""
|
||||
|
||||
def test_create_criterion_score(self):
|
||||
"""Ein Kriterium-Score sollte erstellt werden koennen."""
|
||||
score = CriterionScore(
|
||||
score=85,
|
||||
weight=0.4,
|
||||
annotations=["Gute Argumentation"],
|
||||
comment="Insgesamt gut",
|
||||
ai_suggestions=["Mehr Beispiele hinzufuegen"]
|
||||
)
|
||||
assert score.score == 85
|
||||
assert score.weight == 0.4
|
||||
|
||||
def test_weighted_score_calculation(self):
|
||||
"""Der gewichtete Score sollte korrekt berechnet werden."""
|
||||
score = CriterionScore(
|
||||
score=80,
|
||||
weight=0.4,
|
||||
annotations=[],
|
||||
comment="",
|
||||
ai_suggestions=[]
|
||||
)
|
||||
weighted = score.score * score.weight
|
||||
assert weighted == 32.0
|
||||
|
||||
|
||||
class TestExpectationHorizon:
|
||||
"""Tests fuer den Erwartungshorizont."""
|
||||
|
||||
def test_create_aufgabe(self):
|
||||
"""Eine Aufgabe sollte erstellt werden koennen."""
|
||||
aufgabe = Aufgabe(
|
||||
id="aufg-1",
|
||||
nummer="1a",
|
||||
text="Analysieren Sie das Gedicht.",
|
||||
operator="analysieren",
|
||||
anforderungsbereich=2,
|
||||
erwartete_leistungen=["Epoche erkennen", "Stilmittel benennen"],
|
||||
punkte=20
|
||||
)
|
||||
assert aufgabe.anforderungsbereich == 2
|
||||
assert aufgabe.punkte == 20
|
||||
|
||||
def test_create_erwartungshorizont(self):
|
||||
"""Ein Erwartungshorizont sollte erstellt werden koennen."""
|
||||
aufgaben = [
|
||||
Aufgabe(id="a1", nummer="1", text="Aufgabe 1", operator="analysieren", anforderungsbereich=2,
|
||||
erwartete_leistungen=["Test"], punkte=30),
|
||||
Aufgabe(id="a2", nummer="2", text="Aufgabe 2", operator="erlaeutern", anforderungsbereich=2,
|
||||
erwartete_leistungen=["Test2"], punkte=30),
|
||||
Aufgabe(id="a3", nummer="3", text="Aufgabe 3", operator="beurteilen", anforderungsbereich=3,
|
||||
erwartete_leistungen=["Test3"], punkte=40),
|
||||
]
|
||||
ewh = Erwartungshorizont(
|
||||
id="ewh-1",
|
||||
aufgaben=aufgaben,
|
||||
max_points=100,
|
||||
hinweise="Allgemeine Hinweise",
|
||||
generated=False,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
assert ewh.max_points == 100
|
||||
assert len(ewh.aufgaben) == 3
|
||||
total_points = sum(a.punkte for a in ewh.aufgaben)
|
||||
assert total_points == 100
|
||||
|
||||
|
||||
class TestKlausurDB:
|
||||
"""Tests fuer die In-Memory Datenbank."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup vor jedem Test - leere die DB."""
|
||||
klausuren_db.clear()
|
||||
|
||||
def test_empty_db(self):
|
||||
"""Eine leere DB sollte leer sein."""
|
||||
assert len(klausuren_db) == 0
|
||||
|
||||
def test_add_klausur_to_db(self):
|
||||
"""Eine Klausur sollte zur DB hinzugefuegt werden koennen."""
|
||||
now = datetime.now()
|
||||
klausur = AbiturKlausur(
|
||||
id="test-1",
|
||||
title="Test Klausur",
|
||||
subject="deutsch",
|
||||
modus=KlausurModus.VORABITUR,
|
||||
year=2025,
|
||||
semester="Q3",
|
||||
kurs="GK",
|
||||
class_id=None,
|
||||
status=KlausurStatus.DRAFT,
|
||||
text_sources=[],
|
||||
erwartungshorizont=None,
|
||||
students=[],
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
klausuren_db["test-1"] = klausur
|
||||
assert "test-1" in klausuren_db
|
||||
assert klausuren_db["test-1"].title == "Test Klausur"
|
||||
|
||||
|
||||
class TestKlausurModus:
|
||||
"""Tests fuer die Klausur-Modi."""
|
||||
|
||||
def test_landes_abitur_mode(self):
|
||||
"""Landes-Abitur Modus sollte existieren."""
|
||||
assert KlausurModus.LANDES_ABITUR.value == "landes_abitur"
|
||||
|
||||
def test_vorabitur_mode(self):
|
||||
"""Vorabitur Modus sollte existieren."""
|
||||
assert KlausurModus.VORABITUR.value == "vorabitur"
|
||||
|
||||
|
||||
class TestTextSourceStatus:
|
||||
"""Tests fuer den TextSource-Status."""
|
||||
|
||||
def test_pending_status(self):
|
||||
"""Pending Status sollte existieren."""
|
||||
assert TextSourceStatus.PENDING.value == "pending"
|
||||
|
||||
def test_verified_status(self):
|
||||
"""Verified Status sollte existieren."""
|
||||
assert TextSourceStatus.VERIFIED.value == "verified"
|
||||
|
||||
def test_rejected_status(self):
|
||||
"""Rejected Status sollte existieren."""
|
||||
assert TextSourceStatus.REJECTED.value == "rejected"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user