2026-06-08 08:10:53 +02:00
|
|
|
|
"""Tests Planungs-KI Phase F — Progressions-Roadmap Pipeline."""
|
|
|
|
|
|
from planning_progression_roadmap import (
|
|
|
|
|
|
PROMPT_SLUG_GOAL_ANALYSIS,
|
|
|
|
|
|
PROMPT_SLUG_ROADMAP,
|
|
|
|
|
|
PROMPT_SLUG_STAGE_SPEC,
|
2026-06-09 12:54:08 +02:00
|
|
|
|
PROMPT_SLUG_START_TARGET,
|
2026-06-08 12:40:17 +02:00
|
|
|
|
MajorStep,
|
2026-06-09 11:10:46 +02:00
|
|
|
|
RoadmapStructuredInput,
|
2026-06-08 12:40:17 +02:00
|
|
|
|
StageSpecArtifact,
|
2026-06-08 08:10:53 +02:00
|
|
|
|
build_goal_analysis,
|
2026-06-08 12:40:17 +02:00
|
|
|
|
build_roadmap_unfilled_gap_specs,
|
2026-06-08 08:10:53 +02:00
|
|
|
|
consolidate_micro_to_major,
|
|
|
|
|
|
develop_micro_objectives,
|
2026-06-09 11:10:46 +02:00
|
|
|
|
parse_start_target_from_goal_query,
|
2026-06-08 08:10:53 +02:00
|
|
|
|
progression_roadmap_to_api_dict,
|
2026-06-09 12:54:08 +02:00
|
|
|
|
resolve_roadmap_structured_input,
|
2026-06-08 12:40:17 +02:00
|
|
|
|
resolve_step_exercise_kind_filter,
|
2026-06-08 08:10:53 +02:00
|
|
|
|
run_progression_roadmap_pipeline,
|
2026-06-09 16:22:16 +02:00
|
|
|
|
run_start_target_resolve_only,
|
2026-06-08 12:40:17 +02:00
|
|
|
|
stage_spec_exercise_kind_filter,
|
|
|
|
|
|
stage_spec_retrieval_query,
|
2026-06-08 14:59:24 +02:00
|
|
|
|
normalize_major_steps_for_override,
|
|
|
|
|
|
roadmap_context_from_override,
|
|
|
|
|
|
RoadmapOverridePayload,
|
2026-06-08 08:10:53 +02:00
|
|
|
|
)
|
|
|
|
|
|
from planning_exercise_semantics import build_semantic_brief
|
|
|
|
|
|
|
2026-06-09 11:10:46 +02:00
|
|
|
|
KUMITE_GOAL = (
|
|
|
|
|
|
"Kumite Beinarbeit von einer gleichartigen Steppbewegung bis zur dynamischen "
|
|
|
|
|
|
"unvorhersehbaren Bewegung mit explosivartigem Angriff und ausweichen"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-08 08:10:53 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 12:40:17 +02:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 14:59:24 +02:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 08:10:53 +02:00
|
|
|
|
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)
|
2026-06-09 12:54:08 +02:00
|
|
|
|
assert api["prompt_slug_catalog"]["start_target"] == PROMPT_SLUG_START_TARGET
|
2026-06-08 08:10:53 +02:00
|
|
|
|
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"] == []
|
2026-06-09 11:10:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 12:54:08 +02:00
|
|
|
|
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 "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 16:22:16 +02:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 12:54:08 +02:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 11:10:46 +02:00
|
|
|
|
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)
|