"""Tests Katalog-Prompt-Slots (H2).""" from unittest.mock import MagicMock from catalog_prompt_slots import ( build_catalog_guidance_for_prompt, pick_active_catalog_item, placeholder_key, resolve_catalog_prompt_variables, ) from planning_catalog_context import PlanningCatalogContextItem, ProgressionPlanningCatalogContext from planning_prompt_variables import merge_planning_prompt_variables def _mock_cur( rows_by_table=None, slots_by_kind_id=None, slot_types_ready=True, ): rows_by_table = rows_by_table or {} slots_by_kind_id = slots_by_kind_id or {} cur = MagicMock() def execute(sql, params=None): sql_l = (sql or "").lower() if "to_regclass" in sql_l: cur.fetchone.return_value = {"t": "catalog_prompt_slot_types" if slot_types_ready else None} return if "from catalog_prompt_slot_types" in sql_l: cur.fetchall.return_value = [] return if "from catalog_prompt_slots" in sql_l: kind, cid = params[0], int(params[1]) slot_map = slots_by_kind_id.get((kind, cid), {}) cur.fetchall.return_value = [ {"slot_key": k, "content": v} for k, v in slot_map.items() ] return for table, rows in rows_by_table.items(): if f"from {table}" in sql_l: item_id = int(params[0]) raw = rows.get(item_id) if raw is None: cur.fetchone.return_value = None elif isinstance(raw, dict): cur.fetchone.return_value = { "id": item_id, "name": raw.get("name", ""), "description": raw.get("description", ""), } else: cur.fetchone.return_value = { "id": item_id, "name": str(raw), "description": "", } return cur.fetchone.return_value = None cur.fetchall.return_value = [] cur.execute.side_effect = execute return cur def test_pick_active_catalog_item_primary_wins(): items = [ PlanningCatalogContextItem(id=1, is_primary=False, weight=0.9), PlanningCatalogContextItem(id=2, is_primary=True, weight=0.5), ] assert pick_active_catalog_item(items).id == 2 def test_granular_placeholder_focus_area_hints_on_path_qa(): cur = _mock_cur( rows_by_table={"focus_areas": {4: {"name": "Gewaltschutz"}}}, slots_by_kind_id={ ("focus_area", 4): { "description": "Planung zielt auf Prävention und Deeskalation.", "hints_on_path_qa": "Lücken sind fehlende Deeskalations-Stufen.", "anti_patterns": "Nicht nach Kumite-Tiefe bewerten.", } }, ) catalog = ProgressionPlanningCatalogContext( focus_areas=[PlanningCatalogContextItem(id=4, is_primary=True)], ) resolved = resolve_catalog_prompt_variables(cur, catalog, slug="planning_exercise_path_qa") assert "Deeskalation" in resolved[placeholder_key("focus_area", "hints_on_path_qa")] assert "Deeskalation" in resolved["catalog_guidance_block"] assert resolved["has_catalog_guidance"] == "true" def test_unknown_focus_uses_default_description_pack(): cur = _mock_cur( rows_by_table={ "focus_areas": { 4: { "name": "Sonderfokus Alpha", "description": "Kurze Stammdaten-Beschreibung", } } }, slots_by_kind_id={("focus_area", 4): {}}, ) catalog = ProgressionPlanningCatalogContext( focus_areas=[PlanningCatalogContextItem(id=4, is_primary=True)], ) resolved = resolve_catalog_prompt_variables(cur, catalog) desc = resolved[placeholder_key("focus_area", "description")] assert "Technik- oder Themen-Curriculum" in desc assert resolved[placeholder_key("focus_area", "hints_on_path_qa")] def test_empty_without_catalog(): cur = MagicMock() out = build_catalog_guidance_for_prompt(cur, None) assert out["has_catalog_guidance"] is False assert out["catalog_guidance_block"] == "" def test_unknown_entry_gets_default_technique_fallback(): cur = _mock_cur(rows_by_table={"focus_areas": {99: {"name": "Unbekannter Fokus XYZ"}}}) catalog = ProgressionPlanningCatalogContext( focus_areas=[PlanningCatalogContextItem(id=99, is_primary=True)], ) out = build_catalog_guidance_for_prompt(cur, catalog) assert out["has_catalog_guidance"] is True assert "Unbekannter Fokus XYZ" in out["catalog_context_json"] assert "Zwischenstufen" in out["catalog_guidance_block"] or "Progression" in out["catalog_guidance_block"] def test_merge_planning_prompt_variables_granular_keys(): cur = _mock_cur( rows_by_table={"focus_areas": {4: {"name": "Gewaltschutz"}}}, slots_by_kind_id={ ("focus_area", 4): {"hints_on_path_qa": "Deeskalation und Grenzen."} }, ) catalog = ProgressionPlanningCatalogContext( focus_areas=[PlanningCatalogContextItem(id=4, is_primary=True)], ) merged = merge_planning_prompt_variables( cur, {"goal_query": "Deeskalation Kinder"}, catalog=catalog, slug="planning_exercise_path_qa", ) assert merged[placeholder_key("focus_area", "hints_on_path_qa")].startswith("Deeskalation") assert merged["has_catalog_guidance"] == "true" def test_priority_order_in_guidance_block(): cur = _mock_cur( rows_by_table={ "focus_areas": {1: {"name": "Gewaltschutz"}}, "training_types": {2: {"name": "Breitensport"}}, }, slots_by_kind_id={ ("focus_area", 1): {"description": "Fokus-Text"}, ("training_type", 2): {"description": "Stil-Text"}, }, ) catalog = ProgressionPlanningCatalogContext( focus_areas=[PlanningCatalogContextItem(id=1, is_primary=True)], training_types=[PlanningCatalogContextItem(id=2, is_primary=True)], ) block = build_catalog_guidance_for_prompt(cur, catalog)["catalog_guidance_block"] assert block.index("Primärfokus") < block.index("Trainingsstil")