"""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_includes_entry_state_from_prior_steps(): brief = build_semantic_brief("Kumite Beinarbeit") steps = [ { "roadmap_major_step_index": 0, "title": "Schritt A", "roadmap_target_state": "gleichmäßige Distanz", "success_criteria": ["Partnerabstand stabil"], }, {"roadmap_major_step_index": 2, "title": "Schritt C"}, ] offer = build_gap_fill_offer( spec={ "source": "roadmap_unfilled", "phase": "vertiefung", "title_hint": "Rhythmen", "roadmap_major_step_index": 1, }, steps=steps, goal_query="Kumite Beinarbeit", brief=brief, roadmap_snapshot={ "start_situation": "Steppbewegung", "stage_learning_goal": "variable Rhythmen", }, ) assert offer["context_preview"]["entry_state"] == "gleichmäßige Distanz" assert "Eingangszustand" 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"]