scripts/test_plans_wp15.py aktualisiert
All checks were successful
Deploy Trainer_LLM to llm-node / deploy (push) Successful in 2s

This commit is contained in:
Lars 2025-08-12 12:57:01 +02:00
parent 4d67cd9d66
commit 3806f4ac47

View File

@ -1,16 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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 os
import requests
import json import json
import requests
import pytest
from datetime import datetime, timezone from datetime import datetime, timezone
BASE = os.getenv("BASE_URL", "http://127.0.0.1:8000").rstrip("/") 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 import hashlib
core = { core = {
"title": plan_payload["title"], "title": plan_payload["title"],
@ -25,9 +28,21 @@ def _fingerprint_local(plan_payload: dict) -> str:
return hashlib.sha256(raw.encode("utf-8")).hexdigest() 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 = { p = {
"title": "Montag - Reaktion", "title": "Montag Reaktion",
"total_minutes": 90, "total_minutes": 90,
"sections": [ "sections": [
{"name": "Warmup", "minutes": 15, "items": [ {"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)) p2 = json.loads(json.dumps(p, ensure_ascii=False))
fp2 = _fingerprint_local(p2) fp2 = _fp_local(p2)
assert fp1 == fp2 assert fp1 == fp2
# ---------- Integration / E2E ---------- # ---------- Integration: Templates mit mehreren Sections + ideal/supplement ----------
def test_template_plan_e2e(): def test_template_sections_ideal_supplement_roundtrip():
# 1) Template anlegen
tpl = { tpl = {
"name": "Standard 90min", "discipline": "Karate", "age_group": "Teenager", "name": "Std 90 v1.1",
"target_group": "Breitensport", "total_minutes": 90, "discipline": "Karate",
"age_group": "Teenager",
"target_group": "Breitensport",
"total_minutes": 90,
"sections": [ "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"], "goals": ["Technik", "Kondition"],
"created_by": "tester", "version": "1.0" "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) 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"] tpl_id = r1.json()["id"]
# 2) Plan anlegen (aus Template, minimal) plan_base = {
plan = {
"template_id": tpl_id, "template_id": tpl_id,
"title": "KW32 Montag", "title": "KW32 Montag",
"discipline": "Karate", "discipline": "Karate",
@ -70,23 +174,33 @@ def test_template_plan_e2e():
{"exercise_external_id": "ex:001", "duration": 10, "why": "Aufwärmen"} {"exercise_external_id": "ex:001", "duration": 10, "why": "Aufwärmen"}
]} ]}
], ],
"goals": ["Technik", "Kondition"],
"capability_summary": {"Reaktionsfähigkeit": 2},
"created_by": "tester", "created_by": "tester",
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(timezone.utc).isoformat(),
"source": "test" "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 assert r2.status_code == 200, r2.text
plan_id = r2.json()["id"] plan_id = r2.json()["id"]
# 3) Idempotenz gleicher Plan → gleiche ID # Plan B gleicher Fingerprint (gleiche items), aber andere Nebenfelder
r3 = requests.post(f"{BASE}/plan", json=plan) 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.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}") r4 = requests.get(f"{BASE}/plan/{plan_id}")
assert r4.status_code == 200 assert r4.status_code == 200
js = r4.json() js = r4.json()
assert js["title"] == "KW32 Montag" # EN-Dash () assert js["title"] == "KW32 Montag"