From 3806f4ac47cd5e3d8a32acb9c9eb13c2754dabcb Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 12 Aug 2025 12:57:01 +0200 Subject: [PATCH] scripts/test_plans_wp15.py aktualisiert --- scripts/test_plans_wp15.py | 168 +++++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 27 deletions(-) diff --git a/scripts/test_plans_wp15.py b/scripts/test_plans_wp15.py index 5d47f17..910cb35 100644 --- a/scripts/test_plans_wp15.py +++ b/scripts/test_plans_wp15.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""Unit-, Integrations- und Mini-E2E-Tests für WP-15.""" +"""Unit-, Integrations- und E2E-Tests für WP-15 (v1.2.0).""" import os -import requests import json +import requests +import pytest from datetime import datetime, timezone BASE = os.getenv("BASE_URL", "http://127.0.0.1:8000").rstrip("/") +QDRANT = os.getenv("QDRANT_BASE", "http://127.0.0.1:6333").rstrip("/") +TPL_COLL = os.getenv("PLAN_TEMPLATE_COLLECTION", "plan_templates") -# ---------- Unit ---------- +# ---------- Helpers ---------- -def _fingerprint_local(plan_payload: dict) -> str: +def _fp_local(plan_payload: dict) -> str: import hashlib core = { "title": plan_payload["title"], @@ -25,9 +28,21 @@ def _fingerprint_local(plan_payload: dict) -> str: return hashlib.sha256(raw.encode("utf-8")).hexdigest() -def test_fingerprint_unit(): +def _unique_len_lower(xs): + seen = set() + out = [] + for x in xs: + k = x.casefold() + if k not in seen: + seen.add(k) + out.append(x) + return len(out) + +# ---------- Unit ---------- + +def test_fingerprint_unit_v12(): p = { - "title": "Montag - Reaktion", + "title": "Montag – Reaktion", "total_minutes": 90, "sections": [ {"name": "Warmup", "minutes": 15, "items": [ @@ -35,30 +50,119 @@ def test_fingerprint_unit(): ]} ], } - fp1 = _fingerprint_local(p) + fp1 = _fp_local(p) p2 = json.loads(json.dumps(p, ensure_ascii=False)) - fp2 = _fingerprint_local(p2) + fp2 = _fp_local(p2) assert fp1 == fp2 -# ---------- Integration / E2E ---------- +# ---------- Integration: Templates mit mehreren Sections + ideal/supplement ---------- -def test_template_plan_e2e(): - # 1) Template anlegen +def test_template_sections_ideal_supplement_roundtrip(): tpl = { - "name": "Standard 90min", "discipline": "Karate", "age_group": "Teenager", - "target_group": "Breitensport", "total_minutes": 90, + "name": "Std 90 v1.1", + "discipline": "Karate", + "age_group": "Teenager", + "target_group": "Breitensport", + "total_minutes": 90, "sections": [ - {"name": "Warmup", "target_minutes": 15, "must_keywords": ["Reaktion"], "forbid_keywords": [], "capability_targets": {"Reaktionsfähigkeit": 2}} + { + "name": "Warmup", + "target_minutes": 15, + "must_keywords": ["Reaktion", "reaktion"], # Duplikat in anderer Schreibweise + "ideal_keywords": ["Koordination", "koordination"], # Duplikat in anderer Schreibweise + "supplement_keywords": ["Teamspiel", " Teamspiel "], # Duplikat mit Whitespace + "forbid_keywords": [], + "capability_targets": {"Reaktionsfähigkeit": 2, "Mobilität": 1} + }, + { + "name": "Technikblock", + "target_minutes": 30, + "must_keywords": ["Mae-Geri"], + "ideal_keywords": ["Timing"], + "supplement_keywords": ["Partnerarbeit"], + "forbid_keywords": ["Bodenarbeit"], + "capability_targets": {"Technikpräzision": 2, "Schnelligkeit": 1} + } ], - "goals": ["Technik", "Kondition"], "equipment_allowed": ["Bälle"], - "created_by": "tester", "version": "1.0" + "goals": ["Technik", "Kondition"], + "equipment_allowed": ["Bälle"], + "created_by": "tester", + "version": "1.1" + } + + r = requests.post(f"{BASE}/plan_templates", json=tpl) + assert r.status_code == 200, r.text + tpl_id = r.json()["id"] + + r2 = requests.get(f"{BASE}/plan_templates/{tpl_id}") + assert r2.status_code == 200, r2.text + got = r2.json() + + # Prüfen: Beide Sections vorhanden + assert len(got["sections"]) == 2 + + s1 = got["sections"][0] + # Normalisierung: Duplikate entfernt (case-insensitive), Whitespace getrimmt + assert _unique_len_lower(s1["must_keywords"]) == len(s1["must_keywords"]) == 1 + assert _unique_len_lower(s1["ideal_keywords"]) == len(s1["ideal_keywords"]) == 1 + assert _unique_len_lower(s1["supplement_keywords"]) == len(s1["supplement_keywords"]) == 1 + +# ---------- Optional: Qdrant-Payload-Check (materialisierte Felder) ---------- + +def test_qdrant_materialized_fields_template(): + # Wenn Qdrant nicht erreichbar ist, Test überspringen + try: + rq = requests.post( + f"{QDRANT}/collections/{TPL_COLL}/points/scroll", + json={"with_payload": True, "limit": 1}, + timeout=2.0, + ) + except Exception: + pytest.skip("Qdrant nicht erreichbar – überspringe materialisierte Feldprüfung") + + if rq.status_code != 200: + pytest.skip(f"Qdrant-Scroll liefert {rq.status_code}") + + js = rq.json() + pts = (js.get("result") or {}).get("points") or [] + if not pts: + pytest.skip("Keine Punkte in plan_templates – überspringe") + + payload = pts[0].get("payload") or {} + # Erwartet: materialisierte Listen vorhanden + for key in [ + "section_names", + "section_must_keywords", + "section_ideal_keywords", + "section_supplement_keywords", + "section_forbid_keywords", + ]: + assert key in payload + assert isinstance(payload[key], list) + +# ---------- E2E: Plan anlegen + Idempotenz trotz variierender Nebenfelder ---------- + +def test_plan_e2e_idempotence_same_fingerprint(): + # Template minimal für Bezug + tpl = { + "name": "Std 90 for plan", + "discipline": "Karate", + "age_group": "Teenager", + "target_group": "Breitensport", + "total_minutes": 90, + "sections": [ + {"name": "Warmup", "target_minutes": 15, "must_keywords": [], "forbid_keywords": [], "capability_targets": {}} + ], + "goals": [], + "equipment_allowed": [], + "created_by": "tester", + "version": "1.0" } r1 = requests.post(f"{BASE}/plan_templates", json=tpl) - assert r1.status_code == 200, r1.text + assert r1.status_code == 200 tpl_id = r1.json()["id"] - # 2) Plan anlegen (aus Template, minimal) - plan = { + plan_base = { "template_id": tpl_id, "title": "KW32 – Montag", "discipline": "Karate", @@ -70,23 +174,33 @@ def test_template_plan_e2e(): {"exercise_external_id": "ex:001", "duration": 10, "why": "Aufwärmen"} ]} ], - "goals": ["Technik", "Kondition"], - "capability_summary": {"Reaktionsfähigkeit": 2}, "created_by": "tester", "created_at": datetime.now(timezone.utc).isoformat(), "source": "test" } - r2 = requests.post(f"{BASE}/plan", json=plan) + + # Plan A mit goals/capability_summary + plan_a = dict(plan_base) + plan_a.update({ + "goals": ["Technik"], + "capability_summary": {"Reaktionsfähigkeit": 2} + }) + r2 = requests.post(f"{BASE}/plan", json=plan_a) assert r2.status_code == 200, r2.text plan_id = r2.json()["id"] - # 3) Idempotenz – gleicher Plan → gleiche ID - r3 = requests.post(f"{BASE}/plan", json=plan) + # Plan B – gleicher Fingerprint (gleiche items), aber andere Nebenfelder + plan_b = dict(plan_base) + plan_b.update({ + "goals": ["Kondition"], + "capability_summary": {"Reaktionsfähigkeit": 3} + }) + r3 = requests.post(f"{BASE}/plan", json=plan_b) assert r3.status_code == 200 - assert r3.json()["id"] == plan_id + assert r3.json()["id"] == plan_id, "Idempotenz verletzt: gleicher Fingerprint muss gleiche ID liefern" - # 4) GET /plan/{id} + # GET prüfen r4 = requests.get(f"{BASE}/plan/{plan_id}") assert r4.status_code == 200 js = r4.json() - assert js["title"] == "KW32 – Montag" # EN-Dash (–) + assert js["title"] == "KW32 – Montag" \ No newline at end of file