shinkan-jinkendo/backend/tests/test_planning_progression_roadmap.py
Lars f2650dac57
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m13s
Enhance Planning Context with Progression Gap Snapshot and Start/Target Analysis
- Introduced `build_progression_gap_snapshot` function to create a compact roadmap context for gap exercises, integrating start situation, target state, and stage specifications.
- Updated `build_gap_fill_goal_text` to include roadmap snapshot details, enhancing the context for AI-generated exercises.
- Enhanced `ProgressionPathSuggestRequest` and related components to support new structured inputs for start/target analysis, improving user experience and AI suggestions.
- Incremented application version to 0.8.212 to reflect these changes.
2026-06-09 16:22:16 +02:00

248 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 1012")
resolved, meta, _ = resolve_roadmap_structured_input(
"Kumite Beinarbeit",
structured,
brief=brief,
include_llm=False,
)
assert resolved.roadmap_notes == "Kindergruppe 1012"
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 1012 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)