"""Tests Auto-Rematch nach Pfad-QS (Phase A).""" from planning_path_rematch import collect_rematch_slot_indices, rematch_roadmap_slots from planning_progression_roadmap import ProgressionRoadmapContext, StageSpecArtifact def _stage_specs(): return [ StageSpecArtifact(major_step_index=0, learning_goal="Grundlage"), StageSpecArtifact(major_step_index=1, learning_goal="Vertiefung"), StageSpecArtifact(major_step_index=2, learning_goal="Anwendung"), ] def test_collect_rematch_slot_indices_from_stripped_with_major_index(): specs = _stage_specs() stripped = [ { "step_index": 1, "roadmap_major_step_index": 1, "issue": "technique_scope", "reasons": ["Passt nicht zur Haupttechnik"], } ] indices, reasons = collect_rematch_slot_indices( stripped_off_topic=stripped, off_topic_steps=[], optimization_hints=[], stage_specs=specs, ) assert indices == {1} assert "Haupttechnik" in reasons[1] def test_collect_rematch_slot_indices_resolves_step_index_to_major(): specs = _stage_specs() off_topic = [ { "step_index": 2, "issue": "stage_mismatch", "reasons": ["Ziel passt nicht"], } ] indices, reasons = collect_rematch_slot_indices( stripped_off_topic=[], off_topic_steps=off_topic, optimization_hints=[], stage_specs=specs, ) assert indices == {2} assert reasons[2] == "Ziel passt nicht" def test_collect_rematch_slot_indices_from_optimization_hints(): specs = _stage_specs() hints = [ { "action": "rematch_slot", "roadmap_major_step_index": 0, "reason": "QS-Tier-1", } ] indices, _ = collect_rematch_slot_indices( stripped_off_topic=[], off_topic_steps=[], optimization_hints=hints, stage_specs=specs, ) assert indices == {0} def test_collect_rematch_slot_indices_from_roadmap_unfilled(): specs = _stage_specs() indices, reasons = collect_rematch_slot_indices( stripped_off_topic=[], off_topic_steps=[], optimization_hints=[], stage_specs=specs, roadmap_unfilled=[(1, specs[1])], ) assert indices == {1} assert "Roadmap-Stufe" in reasons[1] def test_rematch_roadmap_slots_replaces_only_target_slot(): specs = _stage_specs() ctx = ProgressionRoadmapContext( goal_query="Mawashi Geri", max_steps=3, stage_specs=specs, ) steps = [ { "exercise_id": 10, "title": "Slot 0 OK", "roadmap_major_step_index": 0, }, { "exercise_id": 20, "title": "Mae Geri falsch", "roadmap_major_step_index": 1, }, { "exercise_id": 30, "title": "Slot 2 OK", "roadmap_major_step_index": 2, }, ] def _fake_match(cur, *, stage_spec, used, **kwargs): assert stage_spec.major_step_index == 1 assert 20 in used assert 10 in used assert 30 in used return ( { "exercise_id": 21, "title": "Sprungkraft Mawashi", "roadmap_major_step_index": 1, }, None, ) ordered, log, unfilled = rematch_roadmap_slots( None, tenant=None, body=None, goal_query="Mawashi Geri", max_steps=3, semantic_brief=None, path_target_profile=None, path_intent="", roadmap_ctx=ctx, steps=steps, slot_indices={1}, rematch_reasons={1: "technique_scope"}, match_slot_fn=_fake_match, ) assert len(ordered) == 3 assert ordered[0]["exercise_id"] == 10 assert ordered[1]["exercise_id"] == 21 assert ordered[2]["exercise_id"] == 30 assert len(log) == 1 assert log[0]["action"] == "replaced" assert log[0]["replaced_exercise_id"] == 20 assert log[0]["new_exercise_id"] == 21 assert not unfilled def test_rematch_excludes_replaced_exercise_from_used(): specs = _stage_specs() ctx = ProgressionRoadmapContext( goal_query="Mawashi Geri", max_steps=3, stage_specs=specs, ) steps = [ {"exercise_id": 10, "title": "OK", "roadmap_major_step_index": 0}, {"exercise_id": 99, "title": "Mae Geri", "roadmap_major_step_index": 1}, ] seen_used = [] def _fake_match(cur, *, used, stage_spec, **kwargs): seen_used.append(set(used)) return ( {"exercise_id": 42, "title": "Neu", "roadmap_major_step_index": stage_spec.major_step_index}, None, ) rematch_roadmap_slots( None, tenant=None, body=None, goal_query="Mawashi", max_steps=3, semantic_brief=None, path_target_profile=None, path_intent="", roadmap_ctx=ctx, steps=steps, slot_indices={1}, rematch_reasons={1: "technique_scope"}, match_slot_fn=_fake_match, ) assert 99 in seen_used[0] def test_rematch_unfilled_leaves_placeholder_step(): specs = _stage_specs() ctx = ProgressionRoadmapContext( goal_query="Mae Geri", max_steps=3, stage_specs=specs, ) steps = [ {"exercise_id": 10, "title": "OK", "roadmap_major_step_index": 0}, {"exercise_id": 99, "title": "Falsch", "roadmap_major_step_index": 1}, ] def _no_match(cur, *, stage_spec, **kwargs): return None, stage_spec ordered, log, unfilled = rematch_roadmap_slots( None, tenant=None, body=None, goal_query="Mae Geri", max_steps=3, semantic_brief=None, path_target_profile=None, path_intent="", roadmap_ctx=ctx, steps=steps, slot_indices={1}, rematch_reasons={1: "stage_mismatch"}, match_slot_fn=_no_match, ) assert len(ordered) == 2 slot1 = ordered[1] assert slot1["exercise_id"] is None assert slot1["slot_status"] == "unfilled" assert slot1["roadmap_match_source"] == "unfilled" assert log[0]["action"] == "rematch_unfilled" assert len(unfilled) == 1