All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Implemented functions to resolve neighboring steps based on major indices and build AI context for unfilled roadmap stages. - Enhanced `try_suggest_ai_stage_step` to generate AI proposals for empty roadmap stages, improving user experience in gap filling. - Updated `build_gap_fill_offer` to utilize major step neighbors for better context in offers related to unfilled slots. - Added tests to ensure correct functionality of AI suggestion handling in the context of roadmap gaps. - Incremented application version to reflect these updates.
187 lines
6.5 KiB
Python
187 lines
6.5 KiB
Python
"""Tests Planungs-KI Phase E3 — Lücken-Angebote und Off-Topic."""
|
|
from planning_exercise_path_ai_fill import (
|
|
build_gap_fill_goal_text,
|
|
build_gap_fill_offer,
|
|
collect_gap_fill_specs,
|
|
)
|
|
from planning_exercise_path_qa import parse_llm_suggested_new_exercises, strip_off_topic_steps_from_path
|
|
from planning_exercise_semantics import build_semantic_brief
|
|
|
|
|
|
def test_parse_llm_suggested_new_exercises():
|
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
|
llm_qa = {
|
|
"suggested_new_exercises": [
|
|
{
|
|
"title_hint": "Mae Geri Kraft am Sandsack",
|
|
"sketch": "Kraft und Schnelligkeit",
|
|
"phase": "vertiefung",
|
|
"insert_after_step_index": 1,
|
|
"rationale": "Zwischenschritt",
|
|
}
|
|
]
|
|
}
|
|
specs = parse_llm_suggested_new_exercises(llm_qa, brief=brief, step_count=5)
|
|
assert len(specs) == 1
|
|
assert specs[0]["insert_after_index"] == 1
|
|
assert "Mae Geri" in specs[0]["title_hint"]
|
|
|
|
|
|
def test_collect_gap_fill_specs_off_topic_and_unfilled():
|
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
|
steps = [
|
|
{"exercise_id": 1, "title": "Mae Geri Kihon"},
|
|
{"exercise_id": 2, "title": "Präzision"},
|
|
{"exercise_id": 3, "title": "One Leg Squat"},
|
|
{"exercise_id": 4, "title": "Gleichgewichtstritt"},
|
|
]
|
|
unfilled = [
|
|
{
|
|
"from_exercise_id": 2,
|
|
"to_exercise_id": 3,
|
|
"expected_phase": "vertiefung",
|
|
"from_title": "Präzision",
|
|
"to_title": "One Leg Squat",
|
|
}
|
|
]
|
|
off_topic = [
|
|
{
|
|
"step_index": 2,
|
|
"exercise_id": 3,
|
|
"title": "One Leg Squat",
|
|
"expected_phase": "vertiefung",
|
|
}
|
|
]
|
|
specs = collect_gap_fill_specs(
|
|
steps=steps,
|
|
unfilled_gaps=unfilled,
|
|
off_topic_steps=off_topic,
|
|
llm_specs=[],
|
|
brief=brief,
|
|
goal_query="Mae Geri Perfektion",
|
|
)
|
|
sources = {s["source"] for s in specs}
|
|
assert "unfilled_gap" in sources
|
|
assert "off_topic" in sources
|
|
off = next(s for s in specs if s["source"] == "off_topic")
|
|
assert off["replace_step_index"] == 2
|
|
assert off["insert_after_index"] == 1
|
|
|
|
|
|
def test_strip_off_topic_steps_from_path():
|
|
steps = [
|
|
{"exercise_id": 1, "title": "A"},
|
|
{"exercise_id": 2, "title": "B"},
|
|
{"exercise_id": 3, "title": "One Leg Squat"},
|
|
{"exercise_id": 4, "title": "D"},
|
|
]
|
|
off_topic = [{"step_index": 2, "title": "One Leg Squat", "exercise_id": 3}]
|
|
out, removed = strip_off_topic_steps_from_path(steps, off_topic)
|
|
assert len(out) == 3
|
|
assert len(removed) == 1
|
|
assert removed[0]["removed_title"] == "One Leg Squat"
|
|
assert [s["exercise_id"] for s in out] == [1, 2, 4]
|
|
|
|
|
|
def test_build_gap_fill_goal_text_includes_topic():
|
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
|
text = build_gap_fill_goal_text(
|
|
goal_query="Mae Geri Perfektion",
|
|
brief=brief,
|
|
spec={"phase": "anwendung", "rationale": "Fehlt Kombinationstraining"},
|
|
step_a={"title": "Kihon"},
|
|
step_b={"title": "Kumite"},
|
|
)
|
|
assert "Mae Geri" in text or "mae geri" in text.lower()
|
|
assert "anwendung" in text
|
|
assert "Kihon" in text
|
|
|
|
|
|
def test_build_gap_fill_goal_text_includes_roadmap_snapshot():
|
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
|
text = build_gap_fill_goal_text(
|
|
goal_query="Kumite Beinarbeit",
|
|
brief=brief,
|
|
spec={"phase": "vertiefung", "title_hint": "variable Rhythmen"},
|
|
step_a={"title": "Schritt A"},
|
|
step_b={"title": "Schritt B"},
|
|
roadmap_snapshot={
|
|
"start_situation": "gleichförmige Steppbewegung",
|
|
"target_state": "explosiver Angriff",
|
|
"stage_learning_goal": "variable Rhythmen und multidirektionale Kontrolle",
|
|
"stage_load_profile": ["timing", "distanz"],
|
|
"skill_hints": ["Beinarbeit"],
|
|
},
|
|
)
|
|
assert "gleichförmige Steppbewegung" in text
|
|
assert "explosiver Angriff" in text
|
|
assert "variable Rhythmen" in text
|
|
assert "timing" in text
|
|
|
|
|
|
def test_build_gap_fill_goal_text_includes_expected_skills():
|
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
|
text = build_gap_fill_goal_text(
|
|
goal_query="Kumite Beinarbeit",
|
|
brief=brief,
|
|
spec={"phase": "vertiefung", "title_hint": "Rhythmen"},
|
|
roadmap_snapshot={
|
|
"expected_skills": [
|
|
{"skill_name": "Timing", "weight": 0.9},
|
|
{"skill_name": "Distanz", "weight": 0.8},
|
|
],
|
|
},
|
|
)
|
|
assert "Erwartete Fähigkeiten" in text
|
|
assert "Timing" in text
|
|
|
|
|
|
def test_build_gap_fill_offer_roadmap_unfilled_uses_major_step_neighbors():
|
|
"""Leere Stufe 2 zwischen Stufe 1 und 3 — Nachbarn per roadmap_major_step_index."""
|
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
|
steps = [
|
|
{
|
|
"title": "Explosive Angriffe",
|
|
"exercise_id": 10,
|
|
"roadmap_major_step_index": 0,
|
|
},
|
|
{
|
|
"title": "Kumite-Anwendung",
|
|
"exercise_id": 30,
|
|
"roadmap_major_step_index": 2,
|
|
},
|
|
]
|
|
offer = build_gap_fill_offer(
|
|
spec={
|
|
"source": "roadmap_unfilled",
|
|
"roadmap_major_step_index": 1,
|
|
"phase": "grundlage",
|
|
"title_hint": "Grundlegende Kumite-Steppbewegungen",
|
|
"gap": {"learning_goal": "Grundlegende Kumite-Steppbewegungen", "expected_phase": "grundlage"},
|
|
},
|
|
steps=steps,
|
|
goal_query="Kumite Beinarbeit",
|
|
brief=brief,
|
|
)
|
|
assert offer["roadmap_major_step_index"] == 1
|
|
assert "Explosive Angriffe" in offer["from_title"]
|
|
assert "Kumite-Anwendung" in offer["to_title"]
|
|
assert "Stufen-Lernziel" in offer["goal_for_ai"] or "Roadmap-Stufe" in offer["goal_for_ai"]
|
|
|
|
|
|
def test_build_gap_fill_offer_exposes_context_preview():
|
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
|
offer = build_gap_fill_offer(
|
|
spec={"source": "roadmap_unfilled", "phase": "vertiefung", "title_hint": "Rhythmen"},
|
|
steps=[{"title": "A"}, {"title": "B"}],
|
|
goal_query="Kumite Beinarbeit",
|
|
brief=brief,
|
|
roadmap_snapshot={
|
|
"start_situation": "Steppbewegung",
|
|
"target_state": "explosiver Angriff",
|
|
"stage_learning_goal": "variable Rhythmen",
|
|
},
|
|
)
|
|
assert offer["context_preview"]["start_situation"] == "Steppbewegung"
|
|
assert "variable Rhythmen" in offer["goal_for_ai"]
|