All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced `slot_assignments` to `ProgressionPathSuggestRequest` for improved handling of existing slot assignments in path building. - Implemented `_slot_assignments_by_major_index` and `_path_step_from_slot_assignment` functions to facilitate the integration of slot assignments into the path generation process. - Updated `_build_steps_roadmap_first` to utilize slot assignments, enhancing the accuracy of path steps based on existing exercise slots. - Enhanced `detect_path_gaps` to skip empty slots, preventing unnecessary errors during gap detection. - Added tests to validate the new slot assignment handling and ensure robustness in path generation logic.
150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
"""Tests Planungs-KI Phase E/F — Pfad-QA."""
|
|
from planning_exercise_path_builder import _pick_best_path_hit
|
|
from planning_exercise_semantics import build_semantic_brief
|
|
from planning_exercise_path_qa import (
|
|
apply_llm_path_reorder,
|
|
detect_path_gaps,
|
|
is_roadmap_planned_neighbor_pair,
|
|
)
|
|
|
|
|
|
def test_pick_best_path_hit_prefers_semantic_score():
|
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
|
hits = [
|
|
{"id": 1, "title": "Mawashi", "score": 0.9, "semantic_score": 0.1},
|
|
{"id": 2, "title": "Mae Geri", "score": 0.75, "semantic_score": 0.85},
|
|
]
|
|
chosen = _pick_best_path_hit(hits, set(), semantic_brief=brief)
|
|
assert chosen["id"] == 2
|
|
|
|
|
|
def test_phrase_compact_match_maegeri():
|
|
from planning_exercise_semantics import _phrase_in_blob
|
|
|
|
assert _phrase_in_blob("mae geri", "Erlernen des Mae-Geri aus Einzelbewegungen")
|
|
assert _phrase_in_blob("mae geri", "Maegeri Kihon")
|
|
|
|
|
|
def test_pick_best_path_hit_fallback_title_only_in_summary():
|
|
from planning_exercise_semantics import pick_best_path_hit
|
|
|
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
|
hits = [
|
|
{
|
|
"id": 1,
|
|
"title": "Kumite Stellungen",
|
|
"summary": "",
|
|
"score": 0.9,
|
|
"semantic_score": 0.02,
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "Einzelbewegungen",
|
|
"summary": "Schrittweise Erlernen des Mae Geri",
|
|
"score": 0.5,
|
|
"semantic_score": 0.08,
|
|
},
|
|
]
|
|
chosen = pick_best_path_hit(hits, set(), semantic_brief=brief)
|
|
assert chosen is not None
|
|
assert int(chosen["id"]) == 2
|
|
|
|
|
|
def test_pick_best_path_hit_skips_used():
|
|
hits = [{"id": 1, "title": "A", "score": 0.5, "semantic_score": 0.5}]
|
|
assert _pick_best_path_hit(hits, {1}) is None
|
|
|
|
|
|
def test_apply_llm_path_reorder_permutation():
|
|
steps = [{"exercise_id": 1}, {"exercise_id": 2}, {"exercise_id": 3}]
|
|
reordered, applied, notes = apply_llm_path_reorder(
|
|
steps,
|
|
{"ordered_step_indices": [0, 2, 1], "sequence_notes": ["Vertiefung vor Anwendung"]},
|
|
)
|
|
assert applied is True
|
|
assert [s["exercise_id"] for s in reordered] == [1, 3, 2]
|
|
assert notes
|
|
|
|
|
|
def test_is_roadmap_planned_neighbor_pair():
|
|
a = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 1}
|
|
b = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 2}
|
|
c = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 4}
|
|
assert is_roadmap_planned_neighbor_pair(a, b) is True
|
|
assert is_roadmap_planned_neighbor_pair(a, c) is False
|
|
assert is_roadmap_planned_neighbor_pair({"exercise_id": 1}, b) is False
|
|
|
|
|
|
def test_detect_path_gaps_skips_roadmap_neighbors():
|
|
brief = build_semantic_brief("Mae Geri")
|
|
steps = [
|
|
{
|
|
"exercise_id": 1,
|
|
"title": "A",
|
|
"roadmap_match_source": "stage_spec",
|
|
"roadmap_major_step_index": 0,
|
|
},
|
|
{
|
|
"exercise_id": 2,
|
|
"title": "B",
|
|
"roadmap_match_source": "stage_spec",
|
|
"roadmap_major_step_index": 1,
|
|
},
|
|
]
|
|
|
|
class _FakeCur:
|
|
def execute(self, *args, **kwargs):
|
|
return None
|
|
|
|
def fetchall(self):
|
|
return []
|
|
|
|
def fetchone(self):
|
|
return {"title": "X", "summary": "", "goal": ""}
|
|
|
|
gaps = detect_path_gaps(_FakeCur(), steps, brief=brief, roadmap_first=True)
|
|
assert gaps == []
|
|
|
|
|
|
def test_detect_path_gaps_skips_empty_slots():
|
|
"""Graph-Bewertung: leere Slots dürfen keinen 500er durch Übergangs-Lücken auslösen."""
|
|
brief = build_semantic_brief("Mawashi Geri Kumite")
|
|
steps = [
|
|
{
|
|
"exercise_id": 10,
|
|
"title": "Stand",
|
|
"roadmap_major_step_index": 0,
|
|
},
|
|
{
|
|
"exercise_id": None,
|
|
"title": "(leer: Slot 2)",
|
|
"is_ai_proposal": True,
|
|
"roadmap_major_step_index": 1,
|
|
},
|
|
{
|
|
"exercise_id": 11,
|
|
"title": "Anwendung",
|
|
"roadmap_major_step_index": 2,
|
|
},
|
|
]
|
|
|
|
class _FakeCur:
|
|
def execute(self, *args, **kwargs):
|
|
return None
|
|
|
|
def fetchall(self):
|
|
return []
|
|
|
|
def fetchone(self):
|
|
return {"title": "X", "summary": "", "goal": ""}
|
|
|
|
gaps = detect_path_gaps(_FakeCur(), steps, brief=brief, roadmap_first=True)
|
|
assert isinstance(gaps, list)
|
|
|
|
|
|
def test_apply_llm_path_reorder_invalid_ignored():
|
|
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
|
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
|
assert applied is False
|
|
assert reordered == steps
|