Update
This commit is contained in:
@@ -1,70 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
from schema_contract import migrate_training_config, validate_training_config
|
||||
|
||||
|
||||
def sample_config():
|
||||
def legacy_config():
|
||||
return {
|
||||
"meta": {"title": "Test", "weeks": 2},
|
||||
"training_format": {"mode": "tabata", "fixed_interval": True, "work_seconds": 20, "rest_seconds": 10, "rounds": 8},
|
||||
"phases": {"items": [{"name": "Start", "weeks": 1}, {"name": "Aufbau", "weeks": 1}]},
|
||||
"days": [{"num": 1, "focus": "A", "rotations": [{"label": "Block", "exercises": [{
|
||||
"name": "Squat-Progression", "key": "squat", "result_schema": {"mode": "reps", "weight_mode": "none", "laterality": "bilateral"}
|
||||
}]}]}],
|
||||
"stages": {"squat": {"name": "Squat-Progression", "steps": [
|
||||
"Deep Squat Hold", "Volle Squats"
|
||||
], "result_schemas": [
|
||||
{"mode": "seconds", "weight_mode": "optional", "laterality": "bilateral"},
|
||||
{"mode": "reps", "weight_mode": "optional", "laterality": "bilateral"},
|
||||
]}},
|
||||
"meta": {
|
||||
"title": "Alter Plan",
|
||||
"subtitle": "wird reduziert",
|
||||
"output_name": "plan.pdf",
|
||||
},
|
||||
"colors": {"ACCENT": "#ff00ff"},
|
||||
"training_format": {"mode": "tabata", "rounds": 8},
|
||||
"phases": {"items": [{"name": "Phase 1"}]},
|
||||
"stages": {"squat": {"name": "Squat", "steps": ["leicht", "schwer"]}},
|
||||
"prepost": {
|
||||
"1": {
|
||||
"warmup": "Handgelenke · Kniebeugen",
|
||||
"cooldown": "Atmen<br/>Locker gehen",
|
||||
"stretch": "alte Dehnung",
|
||||
}
|
||||
},
|
||||
"days": [
|
||||
{
|
||||
"num": 1,
|
||||
"focus": "Beine",
|
||||
"color": "#123456",
|
||||
"rotations": [
|
||||
{
|
||||
"label": "Block A",
|
||||
"exercises": [
|
||||
{
|
||||
"id": "old-placement",
|
||||
"exercise_id": "exercise-squat",
|
||||
"name": "Squat",
|
||||
"key": "squat",
|
||||
"cue": "tief",
|
||||
"result_schema": {"mode": "reps"},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_migration_creates_stable_ids_object_steps_and_catalog():
|
||||
first = migrate_training_config(sample_config(), plan_id="test")
|
||||
def test_migration_flattens_legacy_plan_to_days_and_exercises():
|
||||
migrated = migrate_training_config(legacy_config(), plan_id="test")
|
||||
|
||||
assert migrated["schema_version"] == 4
|
||||
assert migrated["contract_version"] == 3
|
||||
assert migrated["plan_id"] == "test"
|
||||
assert "colors" not in migrated
|
||||
assert "training_format" not in migrated
|
||||
assert "phases" not in migrated
|
||||
assert "stages" not in migrated
|
||||
assert "exercise_catalog" not in migrated
|
||||
|
||||
assert migrated["meta"] == {"title": "Alter Plan", "subtitle": "wird reduziert"}
|
||||
assert migrated["exercises"] == [{"id": "exercise-squat", "name": "Squat", "cue": "tief"}]
|
||||
|
||||
day = migrated["days"][0]
|
||||
assert day["name"] == "Beine"
|
||||
assert day["warmup"]["items"] == ["Handgelenke", "Kniebeugen"]
|
||||
assert day["cooldown"]["items"] == ["Atmen", "Locker gehen"]
|
||||
assert "stretch" not in day
|
||||
assert "rotations" not in day
|
||||
|
||||
item = day["exercises"][0]
|
||||
assert item == {
|
||||
"id": "old-placement",
|
||||
"exercise_id": "exercise-squat",
|
||||
"notes": "Block A",
|
||||
}
|
||||
|
||||
|
||||
def test_migration_is_stable_across_repeated_runs():
|
||||
first = migrate_training_config(legacy_config(), plan_id="test")
|
||||
second = migrate_training_config(first, plan_id="test")
|
||||
assert first["schema_version"] == 3
|
||||
assert first["contract_version"] == 2
|
||||
assert first["days"][0]["id"] == second["days"][0]["id"]
|
||||
exercise = first["days"][0]["rotations"][0]["exercises"][0]
|
||||
assert exercise["id"] == second["days"][0]["rotations"][0]["exercises"][0]["id"]
|
||||
steps = first["stages"]["squat"]["steps"]
|
||||
assert all(isinstance(step, dict) and step.get("id") for step in steps)
|
||||
assert steps[0]["result_schema"]["mode"] == "seconds"
|
||||
assert steps[1]["result_schema"]["mode"] == "reps"
|
||||
assert first["exercise_catalog"]
|
||||
assert first == second
|
||||
|
||||
|
||||
def test_validation_accepts_complete_plan():
|
||||
result = validate_training_config(sample_config())
|
||||
assert result["errors"] == []
|
||||
def test_validation_accepts_complete_minimal_plan():
|
||||
cfg = {
|
||||
"meta": {"title": "Minimal"},
|
||||
"exercises": [{"id": "exercise-pushup", "name": "Push-up"}],
|
||||
"days": [
|
||||
{
|
||||
"id": "day-1",
|
||||
"num": 1,
|
||||
"name": "Tag 1",
|
||||
"warmup": {"items": ["Mobilisieren"]},
|
||||
"cooldown": {"items": ["Atmen"]},
|
||||
"exercises": [{"id": "item-1", "exercise_id": "exercise-pushup", "sets": 3, "reps": "8-12"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
assert validate_training_config(cfg)["errors"] == []
|
||||
|
||||
|
||||
def test_missing_result_schemas_are_migrated_to_explicit_defaults():
|
||||
cfg = sample_config()
|
||||
exercise = cfg["days"][0]["rotations"][0]["exercises"][0]
|
||||
exercise.pop("result_schema", None)
|
||||
cfg["stages"]["squat"].pop("result_schemas", None)
|
||||
migrated = migrate_training_config(cfg, plan_id="test")
|
||||
ex_schema = migrated["days"][0]["rotations"][0]["exercises"][0]["result_schema"]
|
||||
assert ex_schema["mode"] == "reps"
|
||||
steps = migrated["stages"]["squat"]["steps"]
|
||||
assert steps[0]["result_schema"]["mode"] == "seconds"
|
||||
assert steps[1]["result_schema"]["mode"] == "reps"
|
||||
assert validate_training_config(migrated)["errors"] == []
|
||||
def test_validation_rejects_unknown_day_exercise_reference():
|
||||
cfg = {
|
||||
"meta": {"title": "Kaputt"},
|
||||
"exercises": [{"id": "exercise-pushup", "name": "Push-up"}],
|
||||
"days": [
|
||||
{
|
||||
"id": "day-1",
|
||||
"num": 1,
|
||||
"name": "Tag 1",
|
||||
"exercises": [{"id": "item-1", "exercise_id": "missing"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_position_id_is_persisted_across_reordering():
|
||||
migrated = migrate_training_config(sample_config(), plan_id="test")
|
||||
exercise = migrated["days"][0]["rotations"][0]["exercises"][0]
|
||||
legacy_id = exercise["legacy_id"]
|
||||
exercise_id = exercise["id"]
|
||||
migrated["days"][0]["rotations"][0]["exercises"].insert(0, {
|
||||
"name": "Neue Übung", "key": "new", "result_schema": {"mode": "reps"}
|
||||
})
|
||||
remigrated = migrate_training_config(migrated, plan_id="test")
|
||||
original = next(item for item in remigrated["days"][0]["rotations"][0]["exercises"] if item["id"] == exercise_id)
|
||||
assert original["legacy_id"] == legacy_id
|
||||
errors = validate_training_config(cfg)["errors"]
|
||||
assert [error["code"] for error in errors] == ["unknown_exercise"]
|
||||
|
||||
Reference in New Issue
Block a user