"""Tests Planungs-KI Phase F — Progressions-Roadmap Pipeline.""" from planning_progression_roadmap import ( PROMPT_SLUG_GOAL_ANALYSIS, PROMPT_SLUG_ROADMAP, PROMPT_SLUG_STAGE_SPEC, PROMPT_SLUG_START_TARGET, MajorStep, RoadmapStructuredInput, StageSpecArtifact, build_goal_analysis, build_roadmap_unfilled_gap_specs, consolidate_micro_to_major, develop_micro_objectives, parse_start_target_from_goal_query, progression_roadmap_to_api_dict, resolve_roadmap_structured_input, resolve_step_exercise_kind_filter, run_progression_roadmap_pipeline, run_start_target_resolve_only, stage_spec_exercise_kind_filter, stage_spec_retrieval_query, normalize_major_steps_for_override, roadmap_context_from_override, RoadmapOverridePayload, ) from planning_exercise_semantics import build_semantic_brief KUMITE_GOAL = ( "Kumite Beinarbeit von einer gleichartigen Steppbewegung bis zur dynamischen " "unvorhersehbaren Bewegung mit explosivartigem Angriff und ausweichen" ) def test_run_progression_roadmap_pipeline_major_step_count(): ctx = run_progression_roadmap_pipeline( "Von Erlernen bis zur Perfektion des Fußtritts Mae Geri", max_steps=5, ) assert ctx.roadmap is not None assert len(ctx.roadmap.major_steps) == 5 assert len(ctx.roadmap.micro_objectives) >= 6 assert len(ctx.stage_specs) == 5 assert ctx.goal_analysis is not None assert "Mae" in ctx.goal_analysis.primary_topic or "mae" in ctx.goal_analysis.primary_topic.lower() def test_consolidate_micro_to_major_reduces_count(): brief = build_semantic_brief("Mae Geri") ga = build_goal_analysis("Mae Geri Perfektion", brief) micro = develop_micro_objectives(brief, goal_analysis=ga, min_count=8) majors, notes = consolidate_micro_to_major(micro, max_steps=5) assert len(majors) == 5 if len(micro) > 5: assert notes assert all(m.learning_goal for m in majors) def test_major_steps_have_learning_goals(): ctx = run_progression_roadmap_pipeline("Mae Geri Grundlagen", max_steps=3) for step in ctx.roadmap.major_steps: assert step.learning_goal.strip() assert step.consolidates def test_stage_spec_retrieval_query_includes_learning_goal(): brief = build_semantic_brief("Mae Geri Perfektion") spec = StageSpecArtifact( major_step_index=1, learning_goal="Koordination und Präzision vertiefen", load_profile=["präzision"], exercise_type="kihon_einzel", ) major = MajorStep(index=1, phase="vertiefung", learning_goal=spec.learning_goal, consolidates=["m3"]) q = stage_spec_retrieval_query( semantic_brief=brief, goal_query="Mae Geri Perfektion", stage_spec=spec, major_step=major, ) assert "vertiefung" in q.lower() assert "Koordination" in q or "Präzision" in q def test_stage_spec_exercise_kind_filter_maps_combination(): spec = StageSpecArtifact(major_step_index=0, exercise_type="kombination") assert stage_spec_exercise_kind_filter(spec) == ["combination"] assert resolve_step_exercise_kind_filter(spec, ["simple"]) == ["simple"] def test_build_roadmap_unfilled_gap_specs(): brief = build_semantic_brief("Mae Geri") spec = StageSpecArtifact(major_step_index=2, learning_goal="Anwendung im Partnerdrill") major = MajorStep(index=2, phase="anwendung", learning_goal=spec.learning_goal, consolidates=["m5"]) specs = build_roadmap_unfilled_gap_specs( unfilled_specs=[(2, spec)], major_steps_by_index={2: major}, steps=[{"exercise_id": 1, "title": "A"}, {"exercise_id": 2, "title": "B"}], brief=brief, goal_query="Mae Geri", ) assert len(specs) == 1 assert specs[0]["source"] == "roadmap_unfilled" assert specs[0]["phase"] == "anwendung" def test_normalize_major_steps_reindexes(): majors = normalize_major_steps_for_override( [ MajorStep(index=9, phase="einstieg", learning_goal="Einstieg", consolidates=[]), MajorStep(index=8, phase="perfektion", learning_goal="Ziel", consolidates=[]), ], max_steps=5, ) assert len(majors) == 2 assert majors[0].index == 0 assert majors[1].index == 1 def test_roadmap_context_from_override(): brief = build_semantic_brief("Mae Geri Perfektion") override = RoadmapOverridePayload( major_steps=[ MajorStep(index=0, phase="einstieg", learning_goal="Mae Geri Einstieg", consolidates=[]), MajorStep(index=1, phase="grundlage", learning_goal="Stand und Hüfte", consolidates=[]), MajorStep(index=2, phase="perfektion", learning_goal="Präzision unter Belastung", consolidates=[]), ] ) ctx = roadmap_context_from_override( "Mae Geri Perfektion", max_steps=5, semantic_brief=brief, override=override, ) assert ctx.pipeline_phase == "roadmap_v1_edited" assert len(ctx.roadmap.major_steps) == 3 assert len(ctx.stage_specs) == 3 assert ctx.stage_specs[1].learning_goal == "Stand und Hüfte" def test_api_dict_exposes_prompt_slug_catalog(): ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False) api = progression_roadmap_to_api_dict(ctx) assert api["prompt_slug_catalog"]["start_target"] == PROMPT_SLUG_START_TARGET assert api["prompt_slug_catalog"]["goal_analysis"] == PROMPT_SLUG_GOAL_ANALYSIS assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC assert api["prompt_slugs"] == [] def test_resolve_structured_user_overrides_regex(): brief = build_semantic_brief(KUMITE_GOAL) structured = RoadmapStructuredInput( start_situation="Trainer-Start explizit", target_state="Trainer-Ziel explizit", ) resolved, meta, llm_raw = resolve_roadmap_structured_input( KUMITE_GOAL, structured, brief=brief, include_llm=False ) assert llm_raw is None assert resolved.start_situation == "Trainer-Start explizit" assert resolved.target_state == "Trainer-Ziel explizit" assert meta.start_source == "user" assert meta.target_source == "user" def test_resolve_structured_regex_fallback_without_llm(): brief = build_semantic_brief(KUMITE_GOAL) resolved, meta, _ = resolve_roadmap_structured_input( KUMITE_GOAL, None, brief=brief, include_llm=False ) assert meta.start_source == "regex" assert meta.target_source == "regex" assert "Steppbewegung" in (resolved.start_situation or "") assert "dynamischen" in (resolved.target_state or "") def test_run_start_target_resolve_only_no_major_steps(): ctx = run_start_target_resolve_only(KUMITE_GOAL, include_llm_start_target=False) assert ctx.pipeline_phase == "start_target_only" assert ctx.roadmap is None assert ctx.goal_analysis is not None assert "Steppbewegung" in ctx.goal_analysis.start_assumption assert ctx.resolved_structured is not None def test_resolve_structured_merges_user_and_llm_notes(): brief = build_semantic_brief("Kumite Beinarbeit") structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 10–12") resolved, meta, _ = resolve_roadmap_structured_input( "Kumite Beinarbeit", structured, brief=brief, include_llm=False, ) assert resolved.roadmap_notes == "Kindergruppe 10–12" assert meta.notes_source == "user" def test_parse_start_target_kumite_beinarbeit(): start, target = parse_start_target_from_goal_query(KUMITE_GOAL) assert start is not None assert "Steppbewegung" in start assert target is not None assert "dynamischen" in target assert "Angriff" in target def test_build_goal_analysis_uses_parsed_start_target(): brief = build_semantic_brief(KUMITE_GOAL) ga = build_goal_analysis(KUMITE_GOAL, brief) assert "Kumite Beinarbeit" in ga.primary_topic assert "Steppbewegung" in ga.start_assumption assert "dynamischen" in ga.target_state assert "Voraussetzungen der Zielgruppe werden im Progressionsgraphen nicht analysiert" not in ga.start_assumption def test_build_goal_analysis_structured_fields_override(): brief = build_semantic_brief("Kumite Beinarbeit") structured = RoadmapStructuredInput( start_situation="statische Vorwärtsbewegung im Partnerdrill", target_state="explosiver Gegenangriff nach unvorhersehbarer Beinarbeit", roadmap_notes="Kindergruppe 10–12 Jahre", ) ga = build_goal_analysis("Kumite Beinarbeit", brief, structured=structured) assert ga.start_assumption == structured.start_situation assert ga.target_state == structured.target_state assert any("Kindergruppe" in c for c in ga.success_criteria) def test_develop_micro_objectives_start_target_kumite(): brief = build_semantic_brief(KUMITE_GOAL) ga = build_goal_analysis(KUMITE_GOAL, brief) micro = develop_micro_objectives(brief, goal_analysis=ga, min_count=6) titles = [m.title for m in micro] assert any("Ausgang" in t for t in titles) assert any("Ziel" in t for t in titles) assert not any("Einstieg und Orientierung zum Thema" in t for t in titles) def test_pipeline_kumite_major_steps_not_generic_templates(): ctx = run_progression_roadmap_pipeline(KUMITE_GOAL, max_steps=5, include_llm_roadmap=False) goals = [s.learning_goal for s in ctx.roadmap.major_steps] joined = " ".join(goals).lower() assert "kumite beinarbeit" in joined assert "steppbewegung" in joined or "ausgang" in joined assert "dynamisch" in joined or "ziel" in joined assert not any(g == "Grundstellung und Basisbewegung" for g in goals)