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

464 lines
14 KiB
Python

"""
Tests für State Engine.
Testet:
- Phasen-Management
- Antizipations-Regeln
- API Endpoints
"""
import pytest
from datetime import datetime, timedelta
from fastapi.testclient import TestClient
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from main import app
from state_engine import (
AnticipationEngine,
PhaseService,
TeacherContext,
SchoolYearPhase,
ClassSummary,
Event,
TeacherStats,
Suggestion,
SuggestionPriority,
RULES
)
client = TestClient(app)
class TestSchoolYearPhase:
"""Tests für SchoolYearPhase."""
def test_all_phases_defined(self):
"""Testet dass alle 9 Phasen definiert sind."""
phases = list(SchoolYearPhase)
assert len(phases) == 9
assert SchoolYearPhase.ONBOARDING in phases
assert SchoolYearPhase.ARCHIVED in phases
def test_phase_values(self):
"""Testet Phase-Werte."""
assert SchoolYearPhase.ONBOARDING.value == "onboarding"
assert SchoolYearPhase.SEMESTER_END.value == "semester_end"
class TestTeacherContext:
"""Tests für TeacherContext."""
def test_create_context(self):
"""Testet Erstellung eines Kontexts."""
ctx = TeacherContext(
teacher_id="test-teacher",
school_id="test-school",
school_year_id="test-year"
)
assert ctx.teacher_id == "test-teacher"
assert ctx.current_phase == SchoolYearPhase.ONBOARDING
def test_has_completed_milestone(self):
"""Testet Meilenstein-Prüfung."""
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
completed_milestones=["consent_accept", "profile_complete"]
)
assert ctx.has_completed_milestone("consent_accept")
assert not ctx.has_completed_milestone("school_select")
def test_has_learning_units(self):
"""Testet Lerneinheiten-Prüfung."""
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
stats=TeacherStats(learning_units_created=0)
)
assert not ctx.has_learning_units()
ctx.stats.learning_units_created = 5
assert ctx.has_learning_units()
def test_to_dict(self):
"""Testet Konvertierung zu Dict."""
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test"
)
d = ctx.to_dict()
assert "teacher_id" in d
assert "current_phase" in d
assert d["current_phase"] == "onboarding"
class TestPhaseService:
"""Tests für PhaseService."""
def test_get_next_phase(self):
"""Testet nächste Phase."""
service = PhaseService()
assert service.get_next_phase(SchoolYearPhase.ONBOARDING) == SchoolYearPhase.SCHOOL_YEAR_START
assert service.get_next_phase(SchoolYearPhase.SCHOOL_YEAR_START) == SchoolYearPhase.TEACHING_SETUP
assert service.get_next_phase(SchoolYearPhase.ARCHIVED) is None
def test_check_and_transition_onboarding(self):
"""Testet automatischen Übergang von Onboarding."""
service = PhaseService()
# Ohne alle Meilensteine
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.ONBOARDING,
completed_milestones=["school_select", "consent_accept"]
)
assert service.check_and_transition(ctx) is None
# Mit allen Meilensteinen
ctx.completed_milestones.append("profile_complete")
new_phase = service.check_and_transition(ctx)
assert new_phase == SchoolYearPhase.SCHOOL_YEAR_START
def test_can_transition_to(self):
"""Testet Übergangs-Prüfung."""
service = PhaseService()
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.ONBOARDING,
completed_milestones=["school_select", "consent_accept", "profile_complete"]
)
# Erlaubter Übergang
assert service.can_transition_to(ctx, SchoolYearPhase.SCHOOL_YEAR_START)
# Nicht erlaubter Übergang (falsche Reihenfolge)
assert not service.can_transition_to(ctx, SchoolYearPhase.PERFORMANCE_1)
class TestAnticipationEngine:
"""Tests für AnticipationEngine."""
def test_get_suggestions_no_classes(self):
"""Testet Vorschläge ohne Klassen."""
engine = AnticipationEngine()
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.SCHOOL_YEAR_START,
classes=[]
)
suggestions = engine.get_suggestions(ctx)
# Sollte "Erste Klasse anlegen" Vorschlag haben
assert any(s.id == "create_first_class" for s in suggestions)
def test_get_suggestions_priority_order(self):
"""Testet Priorisierung der Vorschläge."""
engine = AnticipationEngine()
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.ONBOARDING,
completed_milestones=[]
)
suggestions = engine.get_suggestions(ctx)
# Vorschläge sollten nach Priorität sortiert sein
if len(suggestions) >= 2:
assert suggestions[0].priority.value <= suggestions[1].priority.value
def test_get_suggestions_max_limit(self):
"""Testet Limit von max. 5 Vorschlägen."""
engine = AnticipationEngine(max_suggestions=5)
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.PERFORMANCE_1,
stats=TeacherStats(
exams_scheduled=5,
exams_graded=0,
unanswered_messages=10
),
upcoming_events=[
Event(
type="exam",
title="Test",
date=datetime.now() + timedelta(days=3),
in_days=3
)
]
)
suggestions = engine.get_suggestions(ctx)
assert len(suggestions) <= 5
def test_get_top_suggestion(self):
"""Testet Top-Vorschlag."""
engine = AnticipationEngine()
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.SCHOOL_YEAR_START,
classes=[]
)
top = engine.get_top_suggestion(ctx)
assert top is not None
assert top.priority == SuggestionPriority.URGENT
def test_suggestions_by_category(self):
"""Testet Gruppierung nach Kategorie."""
engine = AnticipationEngine()
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.ONBOARDING
)
by_category = engine.get_suggestions_by_category(ctx)
assert isinstance(by_category, dict)
class TestRules:
"""Tests für vordefinierte Regeln."""
def test_rules_count(self):
"""Testet Anzahl der Regeln."""
assert len(RULES) >= 15 # Mindestens 15 Regeln
def test_rule_no_classes(self):
"""Testet 'no_classes' Regel."""
rule = next(r for r in RULES if r.id == "no_classes")
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.SCHOOL_YEAR_START,
classes=[]
)
suggestion = rule.evaluate(ctx)
assert suggestion is not None
assert suggestion.id == "create_first_class"
# Mit Klassen sollte keine Suggestion kommen
ctx.classes.append(ClassSummary(
class_id="1", name="7a", grade_level=7, student_count=25, subject="Deutsch"
))
assert rule.evaluate(ctx) is None
def test_rule_phase_restriction(self):
"""Testet Phasen-Einschränkung."""
rule = next(r for r in RULES if r.id == "no_classes")
# In erlaubter Phase
ctx = TeacherContext(
teacher_id="test",
school_id="test",
school_year_id="test",
current_phase=SchoolYearPhase.SCHOOL_YEAR_START,
classes=[]
)
assert rule.evaluate(ctx) is not None
# In nicht erlaubter Phase
ctx.current_phase = SchoolYearPhase.PERFORMANCE_1
assert rule.evaluate(ctx) is None
class TestStateEngineAPI:
"""Tests für State Engine API."""
def test_get_context(self):
"""Testet Context-Abruf."""
response = client.get("/api/state/context?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "context" in data
assert "phase_info" in data
def test_get_phase(self):
"""Testet Phasen-Abruf."""
response = client.get("/api/state/phase?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "current_phase" in data
assert "phase_info" in data
def test_get_all_phases(self):
"""Testet Abruf aller Phasen."""
response = client.get("/api/state/phases")
assert response.status_code == 200
data = response.json()
assert "phases" in data
assert len(data["phases"]) >= 8
def test_get_suggestions(self):
"""Testet Vorschläge-Abruf."""
response = client.get("/api/state/suggestions?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "suggestions" in data
assert "current_phase" in data
assert "priority_counts" in data
def test_get_top_suggestion(self):
"""Testet Top-Vorschlag-Abruf."""
response = client.get("/api/state/suggestions/top?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "suggestion" in data or "message" in data
def test_get_dashboard(self):
"""Testet Dashboard-Abruf."""
response = client.get("/api/state/dashboard?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "context" in data
assert "suggestions" in data
assert "stats" in data
assert "progress" in data
assert "phases" in data
def test_complete_milestone(self):
"""Testet Meilenstein-Abschluss."""
response = client.post(
"/api/state/milestone?teacher_id=milestone-test",
json={"milestone": "test_milestone"}
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["milestone"] == "test_milestone"
assert "test_milestone" in data["completed_milestones"]
def test_get_next_phase(self):
"""Testet nächste Phase."""
response = client.get("/api/state/next-phase?teacher_id=test-api")
assert response.status_code == 200
data = response.json()
assert "current_phase" in data
assert "next_phase" in data or "message" in data
class TestDemoEndpoints:
"""Tests für Demo-Endpoints."""
def test_demo_add_class(self):
"""Testet Demo-Klasse hinzufügen."""
response = client.post(
"/api/state/demo/add-class?teacher_id=demo-test&name=7a&grade_level=7&student_count=25"
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["classes"] >= 1
def test_demo_add_event(self):
"""Testet Demo-Event hinzufügen."""
response = client.post(
"/api/state/demo/add-event?teacher_id=demo-test&event_type=exam&title=Mathe-Test&in_days=7"
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
def test_demo_update_stats(self):
"""Testet Demo-Stats aktualisieren."""
response = client.post(
"/api/state/demo/update-stats?teacher_id=demo-test&learning_units=5&exams_scheduled=3"
)
assert response.status_code == 200
data = response.json()
assert data["stats"]["learning_units_created"] == 5
assert data["stats"]["exams_scheduled"] == 3
def test_demo_reset(self):
"""Testet Demo-Reset."""
response = client.post("/api/state/demo/reset?teacher_id=demo-test")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
class TestPhaseTransitions:
"""Tests für Phasen-Übergänge."""
def test_manual_transition(self):
"""Testet manuellen Übergang."""
# Erst Kontext mit allen Voraussetzungen erstellen
client.post(
"/api/state/milestone?teacher_id=transition-test",
json={"milestone": "school_select"}
)
client.post(
"/api/state/milestone?teacher_id=transition-test",
json={"milestone": "consent_accept"}
)
client.post(
"/api/state/milestone?teacher_id=transition-test",
json={"milestone": "profile_complete"}
)
# Transition sollte automatisch erfolgt sein oder manuell möglich
response = client.get("/api/state/phase?teacher_id=transition-test")
assert response.status_code == 200
def test_invalid_transition(self):
"""Testet ungültigen Übergang."""
response = client.post(
"/api/state/transition?teacher_id=invalid-test",
json={"target_phase": "archived"} # Nicht erlaubt
)
assert response.status_code == 400
def test_invalid_phase_name(self):
"""Testet ungültigen Phasen-Namen."""
response = client.post(
"/api/state/transition?teacher_id=test",
json={"target_phase": "invalid_phase"}
)
assert response.status_code == 400