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>
347 lines
11 KiB
Python
347 lines
11 KiB
Python
"""
|
|
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"])
|