shinkan-jinkendo/backend/tests/test_planning_path_rematch.py
Lars a4e73c830f
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 22s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
Implement Pruning of Filled Steps from Roadmap Unfilled
- Introduced `_prune_filled_from_roadmap_unfilled` function to remove steps with filled exercises from the unfilled roadmap, preventing outdated references.
- Updated `_run_roadmap_rematch_loop` to incorporate the new pruning logic, ensuring only relevant unfilled steps are retained during rematching.
- Added tests for the pruning function to validate its behavior with various step scenarios.
- Bumped version to 0.8.232 to reflect the new functionality and improvements.
2026-06-12 08:27:39 +02:00

238 lines
6.7 KiB
Python

"""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
def test_prune_filled_from_roadmap_unfilled():
from planning_exercise_path_builder import _prune_filled_from_roadmap_unfilled
spec = StageSpecArtifact(major_step_index=5, learning_goal="Zielgenauigkeit")
steps = [{"exercise_id": 99, "roadmap_major_step_index": 5}]
kept = _prune_filled_from_roadmap_unfilled(steps, [(4, spec)])
assert kept == []
unfilled_steps = [{"exercise_id": None, "roadmap_major_step_index": 5}]
kept2 = _prune_filled_from_roadmap_unfilled(unfilled_steps, [(4, spec)])
assert len(kept2) == 1