Codebase hardening: 102 fixes across 35+ files
Deep audit identified 106 findings; 102 fixed, 4 deferred. Covers 8 areas: - Settings & deploy: env-gated DEBUG/SECRET_KEY, HTTPS headers, gunicorn, celery worker - Auth (registered_user): password write_only, request.data fixes, transaction safety, proper HTTP status codes - Workout app: IDOR protection, get_object_or_404, prefetch_related N+1 fixes, transaction.atomic - Video/scripts: path traversal sanitization, HLS trigger guard, auth on cache wipe - Models (exercise/equipment/muscle/superset): null-safe __str__, stable IDs, prefetch support - Generator views: helper for registered_user lookup, logger.exception, bulk_update, transaction wrapping - Generator core (rules/selector/generator): push-pull ratio, type affinity normalization, modality checks, side-pair exact match, word-boundary regex, equipment cache clearing - Generator services (plan_builder/analyzer/normalizer): transaction.atomic, muscle cache, bulk_update, glutes classification fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
63
generator/tests/test_exercise_family_dedup.py
Normal file
63
generator/tests/test_exercise_family_dedup.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from exercise.models import Exercise
|
||||
from generator.models import UserPreference
|
||||
from generator.services.exercise_selector import (
|
||||
ExerciseSelector,
|
||||
extract_movement_families,
|
||||
)
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
|
||||
class TestExerciseFamilyDedup(TestCase):
|
||||
def setUp(self):
|
||||
django_user = User.objects.create_user(
|
||||
username='family_dedup_user',
|
||||
password='testpass123',
|
||||
)
|
||||
registered_user = RegisteredUser.objects.create(
|
||||
user=django_user,
|
||||
first_name='Family',
|
||||
last_name='Dedup',
|
||||
)
|
||||
self.preference = UserPreference.objects.create(
|
||||
registered_user=registered_user,
|
||||
days_per_week=4,
|
||||
fitness_level=2,
|
||||
)
|
||||
|
||||
def test_high_pull_maps_to_clean_family(self):
|
||||
clean_pull_families = extract_movement_families('Barbell Clean Pull')
|
||||
high_pull_families = extract_movement_families('Barbell High Pull')
|
||||
|
||||
self.assertIn('clean', clean_pull_families)
|
||||
self.assertIn('clean', high_pull_families)
|
||||
|
||||
def test_high_pull_blocked_when_clean_family_already_used(self):
|
||||
high_pull = Exercise.objects.create(
|
||||
name='Barbell High Pull',
|
||||
movement_patterns='lower pull,lower pull - hip hinge',
|
||||
muscle_groups='glutes,hamstrings,traps',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
exercise_tier='secondary',
|
||||
complexity_rating=3,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
selector = ExerciseSelector(self.preference)
|
||||
selector.used_movement_families['clean'] = 1
|
||||
|
||||
selected = selector._weighted_pick(
|
||||
Exercise.objects.filter(pk=high_pull.pk),
|
||||
Exercise.objects.none(),
|
||||
count=1,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
selected,
|
||||
[],
|
||||
'High-pull variant should be blocked when clean family is already used.',
|
||||
)
|
||||
142
generator/tests/test_exercise_similarity_dedup.py
Normal file
142
generator/tests/test_exercise_similarity_dedup.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from exercise.models import Exercise
|
||||
from generator.models import UserPreference
|
||||
from generator.services.exercise_selector import ExerciseSelector
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
|
||||
class TestExerciseSimilarityDedup(TestCase):
|
||||
def setUp(self):
|
||||
django_user = User.objects.create_user(
|
||||
username='similarity_dedup_user',
|
||||
password='testpass123',
|
||||
)
|
||||
registered_user = RegisteredUser.objects.create(
|
||||
user=django_user,
|
||||
first_name='Similarity',
|
||||
last_name='Dedup',
|
||||
)
|
||||
self.preference = UserPreference.objects.create(
|
||||
registered_user=registered_user,
|
||||
days_per_week=4,
|
||||
fitness_level=2,
|
||||
)
|
||||
|
||||
def test_hard_similarity_blocks_near_identical_working_exercise(self):
|
||||
selector = ExerciseSelector(self.preference)
|
||||
prior = Exercise.objects.create(
|
||||
name='Posterior Chain Pull Alpha',
|
||||
movement_patterns='lower pull, lower pull - hip hinge',
|
||||
muscle_groups='glutes,hamstrings,traps',
|
||||
equipment_required='barbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
candidate = Exercise.objects.create(
|
||||
name='Posterior Chain Pull Beta',
|
||||
movement_patterns='lower pull, lower pull - hip hinge',
|
||||
muscle_groups='glutes,hamstrings,traps',
|
||||
equipment_required='barbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
selector.used_working_similarity_profiles.append(
|
||||
selector._build_similarity_profile(prior)
|
||||
)
|
||||
selected = selector._weighted_pick(
|
||||
Exercise.objects.filter(pk=candidate.pk),
|
||||
Exercise.objects.none(),
|
||||
count=1,
|
||||
similarity_scope='working',
|
||||
)
|
||||
self.assertEqual(
|
||||
selected,
|
||||
[],
|
||||
'Near-identical exercise should be hard-blocked in same workout.',
|
||||
)
|
||||
|
||||
def test_soft_similarity_blocks_adjacent_superset_repetition(self):
|
||||
selector = ExerciseSelector(self.preference)
|
||||
previous_set_ex = Exercise.objects.create(
|
||||
name='Hip Hinge Pattern Alpha',
|
||||
movement_patterns='lower pull, lower pull - hip hinge, core',
|
||||
muscle_groups='glutes,hamstrings,core',
|
||||
equipment_required='barbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
adjacent_candidate = Exercise.objects.create(
|
||||
name='Hip Hinge Pattern Beta',
|
||||
movement_patterns='lower pull - hip hinge, core',
|
||||
muscle_groups='glutes,hamstrings,core',
|
||||
equipment_required='barbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
selector.last_working_similarity_profiles = [
|
||||
selector._build_similarity_profile(previous_set_ex)
|
||||
]
|
||||
selected = selector._weighted_pick(
|
||||
Exercise.objects.filter(pk=adjacent_candidate.pk),
|
||||
Exercise.objects.none(),
|
||||
count=1,
|
||||
similarity_scope='working',
|
||||
)
|
||||
self.assertEqual(
|
||||
selected,
|
||||
[],
|
||||
'Very similar adjacent-set exercise should be soft-blocked.',
|
||||
)
|
||||
|
||||
def test_dissimilar_exercise_is_allowed(self):
|
||||
selector = ExerciseSelector(self.preference)
|
||||
previous_set_ex = Exercise.objects.create(
|
||||
name='Posterior Chain Pull Alpha',
|
||||
movement_patterns='lower pull, lower pull - hip hinge, core',
|
||||
muscle_groups='glutes,hamstrings,core',
|
||||
equipment_required='barbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
different_candidate = Exercise.objects.create(
|
||||
name='Horizontal Push Builder',
|
||||
movement_patterns='upper push - horizontal, upper push',
|
||||
muscle_groups='chest,triceps,deltoids',
|
||||
equipment_required='dumbbell',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
selector.last_working_similarity_profiles = [
|
||||
selector._build_similarity_profile(previous_set_ex)
|
||||
]
|
||||
selected = selector._weighted_pick(
|
||||
Exercise.objects.filter(pk=different_candidate.pk),
|
||||
Exercise.objects.none(),
|
||||
count=1,
|
||||
similarity_scope='working',
|
||||
)
|
||||
self.assertEqual(len(selected), 1)
|
||||
self.assertEqual(selected[0].pk, different_candidate.pk)
|
||||
103
generator/tests/test_modality_guardrails.py
Normal file
103
generator/tests/test_modality_guardrails.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from exercise.models import Exercise
|
||||
from generator.models import UserPreference
|
||||
from generator.services.exercise_selector import ExerciseSelector
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
|
||||
class TestModalityGuardrails(TestCase):
|
||||
def setUp(self):
|
||||
django_user = User.objects.create_user(
|
||||
username='modality_guardrails_user',
|
||||
password='testpass123',
|
||||
)
|
||||
registered_user = RegisteredUser.objects.create(
|
||||
user=django_user,
|
||||
first_name='Modality',
|
||||
last_name='Guardrails',
|
||||
)
|
||||
self.preference = UserPreference.objects.create(
|
||||
registered_user=registered_user,
|
||||
days_per_week=4,
|
||||
fitness_level=2,
|
||||
)
|
||||
|
||||
def test_rep_mode_excludes_duration_only_exercises(self):
|
||||
duration_only = Exercise.objects.create(
|
||||
name="Dumbbell Waiter's Carry",
|
||||
movement_patterns='core,core - carry',
|
||||
muscle_groups='core,deltoids,upper back',
|
||||
equipment_required='Dumbbell',
|
||||
is_weight=True,
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
is_compound=True,
|
||||
exercise_tier='secondary',
|
||||
difficulty_level='intermediate',
|
||||
complexity_rating=3,
|
||||
)
|
||||
reps_ex = Exercise.objects.create(
|
||||
name='2 Kettlebell Clean and Press',
|
||||
movement_patterns='upper push - vertical, upper push, lower pull',
|
||||
muscle_groups='deltoids,triceps,glutes',
|
||||
equipment_required='Kettlebell',
|
||||
is_weight=True,
|
||||
is_duration=False,
|
||||
is_reps=True,
|
||||
is_compound=True,
|
||||
exercise_tier='secondary',
|
||||
difficulty_level='intermediate',
|
||||
complexity_rating=3,
|
||||
)
|
||||
|
||||
selector = ExerciseSelector(self.preference)
|
||||
picked = selector.select_exercises(
|
||||
muscle_groups=[],
|
||||
count=2,
|
||||
is_duration_based=False,
|
||||
)
|
||||
picked_ids = {e.pk for e in picked}
|
||||
|
||||
self.assertIn(reps_ex.pk, picked_ids)
|
||||
self.assertNotIn(duration_only.pk, picked_ids)
|
||||
|
||||
def test_working_selection_excludes_static_stretch_patterns(self):
|
||||
static_stretch = Exercise.objects.create(
|
||||
name='Supine Pec Stretch - T',
|
||||
movement_patterns='mobility - static, static stretch, cool down',
|
||||
muscle_groups='chest,shoulders',
|
||||
equipment_required='None',
|
||||
is_weight=False,
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
difficulty_level='beginner',
|
||||
complexity_rating=1,
|
||||
)
|
||||
valid_reps = Exercise.objects.create(
|
||||
name='Barbell Clean Pull',
|
||||
movement_patterns='upper pull,hip hinge',
|
||||
muscle_groups='upper back,hamstrings,glutes',
|
||||
equipment_required='Barbell',
|
||||
is_weight=True,
|
||||
is_duration=False,
|
||||
is_reps=True,
|
||||
is_compound=True,
|
||||
exercise_tier='primary',
|
||||
difficulty_level='intermediate',
|
||||
complexity_rating=3,
|
||||
)
|
||||
|
||||
selector = ExerciseSelector(self.preference)
|
||||
picked = selector.select_exercises(
|
||||
muscle_groups=[],
|
||||
count=2,
|
||||
is_duration_based=False,
|
||||
)
|
||||
picked_ids = {e.pk for e in picked}
|
||||
|
||||
self.assertIn(valid_reps.pk, picked_ids)
|
||||
self.assertNotIn(static_stretch.pk, picked_ids)
|
||||
@@ -61,6 +61,18 @@ class MovementEnforcementTestBase(TestCase):
|
||||
superset_size_min=3,
|
||||
superset_size_max=6,
|
||||
)
|
||||
cls.functional_type, _ = WorkoutType.objects.get_or_create(
|
||||
name='functional_strength_training',
|
||||
defaults={
|
||||
'typical_rest_between_sets': 60,
|
||||
'typical_intensity': 'high',
|
||||
'rep_range_min': 6,
|
||||
'rep_range_max': 12,
|
||||
'duration_bias': 0.2,
|
||||
'superset_size_min': 2,
|
||||
'superset_size_max': 4,
|
||||
},
|
||||
)
|
||||
cls.core_type = WorkoutType.objects.filter(name='core_training').first()
|
||||
if cls.core_type is None:
|
||||
cls.core_type = WorkoutType.objects.create(
|
||||
@@ -184,6 +196,54 @@ class TestMovementPatternEnforcement(MovementEnforcementTestBase):
|
||||
|
||||
pref.delete()
|
||||
|
||||
def test_working_superset_filters_stretch_entries_and_keeps_positive_rest(self):
|
||||
"""Working supersets should never keep static stretch entries."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
valid = self._create_mock_exercise(
|
||||
name='Barbell Clean Pull',
|
||||
movement_patterns='upper pull,hip hinge',
|
||||
is_duration=False,
|
||||
is_reps=True,
|
||||
is_weight=True,
|
||||
is_compound=True,
|
||||
)
|
||||
stretch = self._create_mock_exercise(
|
||||
name='Supine Pec Stretch - T',
|
||||
movement_patterns='mobility - static, static stretch, cool down',
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
is_weight=False,
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
)
|
||||
|
||||
gen.exercise_selector.select_exercises.return_value = [valid, stretch]
|
||||
gen.exercise_selector.balance_stretch_positions.return_value = [valid, stretch]
|
||||
|
||||
muscle_split = {
|
||||
'muscles': ['chest', 'upper back'],
|
||||
'split_type': 'upper',
|
||||
'label': 'Upper',
|
||||
}
|
||||
wt_params = dict(WORKOUT_TYPE_DEFAULTS['functional strength'])
|
||||
wt_params['num_supersets'] = (1, 1)
|
||||
wt_params['exercises_per_superset'] = (2, 2)
|
||||
wt_params['rounds'] = (4, 4)
|
||||
|
||||
supersets = gen._build_working_supersets(
|
||||
muscle_split, self.functional_type, wt_params,
|
||||
)
|
||||
self.assertTrue(supersets, 'Expected at least one working superset.')
|
||||
ss = supersets[0]
|
||||
exercise_names = [e['exercise'].name for e in ss.get('exercises', [])]
|
||||
|
||||
self.assertIn('Barbell Clean Pull', exercise_names)
|
||||
self.assertNotIn('Supine Pec Stretch - T', exercise_names)
|
||||
self.assertGreater(ss.get('rest_between_rounds', 0), 0)
|
||||
pref.delete()
|
||||
|
||||
def test_retries_when_superset_has_duplicate_focus(self):
|
||||
"""Generator should retry when a working superset repeats focus family."""
|
||||
pref = self._make_preference()
|
||||
@@ -992,3 +1052,49 @@ class TestFinalConformance(MovementEnforcementTestBase):
|
||||
self.assertIsInstance(workout, dict)
|
||||
self.assertEqual(gen._build_working_supersets.call_count, 1)
|
||||
pref.delete()
|
||||
|
||||
def test_side_pair_warning_filtered_when_final_workout_has_no_side_entries(self):
|
||||
"""Do not surface side-pair warnings when final workout has no sided exercises."""
|
||||
pref = self._make_preference()
|
||||
gen = self._make_generator(pref)
|
||||
|
||||
gen._build_warmup = MagicMock(return_value=None)
|
||||
gen._build_cooldown = MagicMock(return_value=None)
|
||||
gen._check_quality_gates = MagicMock(return_value=[])
|
||||
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
|
||||
|
||||
ex = self._create_mock_exercise('Compound Lift', movement_patterns='upper pull')
|
||||
ex.side = ''
|
||||
gen._build_working_supersets = MagicMock(return_value=[
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 3,
|
||||
'rest_between_rounds': 45,
|
||||
'modality': 'reps',
|
||||
'exercises': [{'exercise': ex, 'reps': 8, 'order': 1}],
|
||||
},
|
||||
])
|
||||
gen._get_final_conformance_violations = MagicMock(return_value=[])
|
||||
gen.exercise_selector.warnings = [
|
||||
'Added 2 missing opposite-side exercise partners.',
|
||||
'Removed 1 unpaired side-specific exercises to enforce left/right pairing.',
|
||||
'Could only find 3/5 exercises for deltoids.',
|
||||
]
|
||||
|
||||
gen.generate_single_workout(
|
||||
muscle_split={
|
||||
'muscles': ['upper back'],
|
||||
'split_type': 'pull',
|
||||
'label': 'Pull Day',
|
||||
},
|
||||
workout_type=self.strength_type,
|
||||
scheduled_date=date(2026, 3, 6),
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
any('Could only find 3/5 exercises for deltoids.' in w for w in gen.warnings)
|
||||
)
|
||||
self.assertFalse(
|
||||
any('opposite-side' in w.lower() or 'side-specific' in w.lower() for w in gen.warnings)
|
||||
)
|
||||
pref.delete()
|
||||
|
||||
60
generator/tests/test_rebalance_replacement_guard.py
Normal file
60
generator/tests/test_rebalance_replacement_guard.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from exercise.models import Exercise
|
||||
from generator.models import UserPreference
|
||||
from generator.services.workout_generator import WorkoutGenerator
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
|
||||
class TestRebalanceReplacementGuard(TestCase):
|
||||
def setUp(self):
|
||||
django_user = User.objects.create_user(
|
||||
username='rebalance_guard_user',
|
||||
password='testpass123',
|
||||
)
|
||||
registered_user = RegisteredUser.objects.create(
|
||||
user=django_user,
|
||||
first_name='Rebalance',
|
||||
last_name='Guard',
|
||||
)
|
||||
self.preference = UserPreference.objects.create(
|
||||
registered_user=registered_user,
|
||||
days_per_week=4,
|
||||
fitness_level=2,
|
||||
)
|
||||
|
||||
def test_pull_replacement_prefers_non_sided_candidates(self):
|
||||
sided_pull = Exercise.objects.create(
|
||||
name='Single Arm Cable Row',
|
||||
side='left_arm',
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=False,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
unsided_pull = Exercise.objects.create(
|
||||
name='Chest Supported Row',
|
||||
side='',
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
is_compound=False,
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
generator = WorkoutGenerator(self.preference)
|
||||
replacement = generator._select_pull_replacement(
|
||||
target_muscles=[],
|
||||
is_duration_based=False,
|
||||
prefer_weighted=False,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(replacement)
|
||||
self.assertEqual(replacement.pk, unsided_pull.pk)
|
||||
self.assertNotEqual(replacement.pk, sided_pull.pk)
|
||||
@@ -548,6 +548,89 @@ class TestValidateWorkout(TestCase):
|
||||
f"Expected superset focus repetition error, got {[v.rule_id for v in violations]}",
|
||||
)
|
||||
|
||||
def test_working_set_rejects_recovery_stretch_movements(self):
|
||||
stretch_ex = _make_exercise(
|
||||
name='Supine Pec Stretch - T',
|
||||
movement_patterns='mobility - static, mobility, cool down',
|
||||
is_reps=False,
|
||||
is_duration=True,
|
||||
)
|
||||
push_ex = _make_exercise(
|
||||
name='Single-Arm Dumbbell Push Press',
|
||||
movement_patterns='upper push - vertical, upper push',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_compound=True,
|
||||
is_weight=True,
|
||||
exercise_tier='secondary',
|
||||
)
|
||||
workout_spec = {
|
||||
'supersets': [
|
||||
_make_superset(name='Warm Up', exercises=[
|
||||
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
||||
], rounds=1),
|
||||
_make_superset(
|
||||
name='Working Set 1',
|
||||
exercises=[
|
||||
_make_entry(exercise=push_ex, reps=8, order=1),
|
||||
_make_entry(exercise=stretch_ex, duration=30, order=2),
|
||||
],
|
||||
rounds=4,
|
||||
),
|
||||
_make_superset(name='Cool Down', exercises=[
|
||||
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
||||
], rounds=1),
|
||||
],
|
||||
}
|
||||
|
||||
violations = validate_workout(
|
||||
workout_spec, 'functional_strength_training', 'general_fitness',
|
||||
)
|
||||
stretch_errors = [
|
||||
v for v in violations
|
||||
if v.rule_id == 'working_contains_recovery' and v.severity == 'error'
|
||||
]
|
||||
self.assertTrue(stretch_errors, 'Expected recovery/stretch error in working set.')
|
||||
|
||||
def test_working_set_requires_positive_rest_between_rounds(self):
|
||||
workout_spec = {
|
||||
'supersets': [
|
||||
_make_superset(name='Warm Up', exercises=[
|
||||
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
||||
], rounds=1),
|
||||
{
|
||||
'name': 'Working Set 1',
|
||||
'rounds': 4,
|
||||
'rest_between_rounds': 0,
|
||||
'exercises': [
|
||||
_make_entry(
|
||||
exercise=_make_exercise(
|
||||
name='Barbell Push Press',
|
||||
movement_patterns='upper push',
|
||||
is_compound=True,
|
||||
is_weight=True,
|
||||
exercise_tier='primary',
|
||||
),
|
||||
reps=5,
|
||||
order=1,
|
||||
),
|
||||
],
|
||||
},
|
||||
_make_superset(name='Cool Down', exercises=[
|
||||
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
||||
], rounds=1),
|
||||
],
|
||||
}
|
||||
|
||||
violations = validate_workout(
|
||||
workout_spec, 'functional_strength_training', 'general_fitness',
|
||||
)
|
||||
rest_warnings = [
|
||||
v for v in violations
|
||||
if v.rule_id == 'working_rest_missing' and v.severity == 'warning'
|
||||
]
|
||||
self.assertTrue(rest_warnings, 'Expected warning for missing/zero working rest.')
|
||||
|
||||
def test_adjacent_focus_repetition_info(self):
|
||||
"""Adjacent working supersets with same focus profile should be advisory."""
|
||||
pull_a = _make_exercise(name='Bicep Curl', movement_patterns='upper pull')
|
||||
|
||||
203
generator/tests/test_side_pair_integrity.py
Normal file
203
generator/tests/test_side_pair_integrity.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from exercise.models import Exercise
|
||||
from generator.models import UserPreference
|
||||
from generator.services.exercise_selector import ExerciseSelector
|
||||
from registered_user.models import RegisteredUser
|
||||
|
||||
|
||||
class TestSidePairIntegrity(TestCase):
|
||||
def setUp(self):
|
||||
django_user = User.objects.create_user(
|
||||
username='side_pair_user',
|
||||
password='testpass123',
|
||||
)
|
||||
registered_user = RegisteredUser.objects.create(
|
||||
user=django_user,
|
||||
first_name='Side',
|
||||
last_name='Pair',
|
||||
)
|
||||
self.preference = UserPreference.objects.create(
|
||||
registered_user=registered_user,
|
||||
days_per_week=4,
|
||||
fitness_level=2,
|
||||
)
|
||||
self.selector = ExerciseSelector(self.preference)
|
||||
|
||||
def test_orphan_left_is_removed_and_replaced(self):
|
||||
left_only = Exercise.objects.create(
|
||||
name='Single Arm Row Left',
|
||||
side='Left',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
filler_a = Exercise.objects.create(
|
||||
name='Chest Supported Row',
|
||||
side='',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
filler_b = Exercise.objects.create(
|
||||
name='Face Pull',
|
||||
side='',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull, rear delt',
|
||||
muscle_groups='upper back,deltoids',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
selected = [left_only]
|
||||
base_qs = Exercise.objects.filter(pk__in=[left_only.pk, filler_a.pk, filler_b.pk])
|
||||
enforced = self.selector._ensure_side_pair_integrity(selected, base_qs, count=1)
|
||||
|
||||
self.assertEqual(len(enforced), 1)
|
||||
self.assertNotEqual(enforced[0].pk, left_only.pk)
|
||||
self.assertIn(
|
||||
enforced[0].pk,
|
||||
{filler_a.pk, filler_b.pk},
|
||||
'Orphan left-side movement should be replaced by a non-sided filler.',
|
||||
)
|
||||
|
||||
def test_left_right_pair_is_preserved(self):
|
||||
left_ex = Exercise.objects.create(
|
||||
name='Single Arm Press Left',
|
||||
side='Left',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper push - vertical, upper push',
|
||||
muscle_groups='deltoids,triceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
right_ex = Exercise.objects.create(
|
||||
name='Single Arm Press Right',
|
||||
side='Right',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper push - vertical, upper push',
|
||||
muscle_groups='deltoids,triceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
enforced = self.selector._ensure_side_pair_integrity(
|
||||
[left_ex, right_ex],
|
||||
Exercise.objects.filter(pk__in=[left_ex.pk, right_ex.pk]),
|
||||
count=2,
|
||||
)
|
||||
enforced_ids = {ex.pk for ex in enforced}
|
||||
self.assertEqual(enforced_ids, {left_ex.pk, right_ex.pk})
|
||||
|
||||
def test_left_arm_right_arm_pair_is_preserved(self):
|
||||
left_ex = Exercise.objects.create(
|
||||
name='Single Arm Row',
|
||||
side='left_arm',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
right_ex = Exercise.objects.create(
|
||||
name='Single Arm Row',
|
||||
side='right_arm',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
paired = self.selector._pair_sided_exercises(
|
||||
[left_ex],
|
||||
Exercise.objects.filter(pk__in=[left_ex.pk, right_ex.pk]),
|
||||
)
|
||||
paired_ids = {ex.pk for ex in paired}
|
||||
self.assertEqual(paired_ids, {left_ex.pk, right_ex.pk})
|
||||
|
||||
def test_orphan_left_arm_is_removed(self):
|
||||
left_ex = Exercise.objects.create(
|
||||
name='Single Arm Row',
|
||||
side='left_arm',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
filler = Exercise.objects.create(
|
||||
name='Inverted Row',
|
||||
side='',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=False,
|
||||
movement_patterns='upper pull - horizontal, upper pull',
|
||||
muscle_groups='lats,upper back,biceps',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
enforced = self.selector._ensure_side_pair_integrity(
|
||||
[left_ex],
|
||||
Exercise.objects.filter(pk__in=[left_ex.pk, filler.pk]),
|
||||
count=1,
|
||||
)
|
||||
self.assertEqual(len(enforced), 1)
|
||||
self.assertEqual(enforced[0].pk, filler.pk)
|
||||
|
||||
def test_try_hard_fetch_adds_opposite_side_partner_from_global_db(self):
|
||||
left_ex = Exercise.objects.create(
|
||||
name='Single Arm Lateral Raise Left',
|
||||
side='Left',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper push',
|
||||
muscle_groups='deltoids',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
right_ex = Exercise.objects.create(
|
||||
name='Single Arm Lateral Raise Right',
|
||||
side='Right',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=True,
|
||||
movement_patterns='upper push',
|
||||
muscle_groups='deltoids',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
filler = Exercise.objects.create(
|
||||
name='Shoulder Tap',
|
||||
side='',
|
||||
is_reps=True,
|
||||
is_duration=False,
|
||||
is_weight=False,
|
||||
movement_patterns='upper push',
|
||||
muscle_groups='deltoids,core',
|
||||
difficulty_level='intermediate',
|
||||
)
|
||||
|
||||
# base_qs intentionally does not include right_ex to validate global fallback.
|
||||
base_qs = Exercise.objects.filter(pk__in=[left_ex.pk, filler.pk])
|
||||
enforced = self.selector._ensure_side_pair_integrity(
|
||||
[left_ex, filler],
|
||||
base_qs,
|
||||
count=2,
|
||||
)
|
||||
enforced_ids = {ex.pk for ex in enforced}
|
||||
self.assertIn(left_ex.pk, enforced_ids)
|
||||
self.assertIn(right_ex.pk, enforced_ids)
|
||||
self.assertNotIn(filler.pk, enforced_ids)
|
||||
@@ -85,8 +85,8 @@ class TestWarmupSelector(TestCase):
|
||||
self.assertNotIn(weighted_press.pk, selected_ids)
|
||||
self.assertNotIn(duration_push.pk, selected_ids)
|
||||
|
||||
def test_warmup_excludes_side_specific_variants(self):
|
||||
left_variant = Exercise.objects.create(
|
||||
def test_warmup_keeps_side_specific_variants_adjacent(self):
|
||||
left_variant_a = Exercise.objects.create(
|
||||
name='Side Lying T Stretch',
|
||||
side='left_arm',
|
||||
movement_patterns='dynamic stretch, mobility - dynamic, warm up',
|
||||
@@ -99,7 +99,7 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
right_variant = Exercise.objects.create(
|
||||
right_variant_a = Exercise.objects.create(
|
||||
name='Side Lying T Stretch',
|
||||
side='right_arm',
|
||||
movement_patterns='dynamic stretch, mobility - dynamic, warm up',
|
||||
@@ -112,9 +112,9 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
unsided_a = Exercise.objects.create(
|
||||
name='Worlds Greatest Stretch',
|
||||
side='',
|
||||
left_variant_b = Exercise.objects.create(
|
||||
name='Quadruped Adductor Stretch with Thoracic Rotation',
|
||||
side='left_side',
|
||||
movement_patterns='dynamic stretch, mobility - dynamic, warm up',
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
@@ -125,31 +125,47 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
unsided_b = Exercise.objects.create(
|
||||
name='Hip Opener Flow',
|
||||
side='',
|
||||
movement_patterns='activation, warmup, cardio/locomotion',
|
||||
right_variant_b = Exercise.objects.create(
|
||||
name='Quadruped Adductor Stretch with Thoracic Rotation',
|
||||
side='right_side',
|
||||
movement_patterns='dynamic stretch, mobility - dynamic, warm up',
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
is_weight=False,
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
hr_elevation_rating=3,
|
||||
complexity_rating=2,
|
||||
hr_elevation_rating=2,
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
|
||||
selector = ExerciseSelector(self.preference)
|
||||
selected = selector.select_warmup_exercises(target_muscles=[], count=2)
|
||||
selected_ids = {ex.pk for ex in selected}
|
||||
selected = selector.select_warmup_exercises(target_muscles=[], count=4)
|
||||
|
||||
self.assertEqual(selected_ids, {unsided_a.pk, unsided_b.pk})
|
||||
self.assertNotIn(left_variant.pk, selected_ids)
|
||||
self.assertNotIn(right_variant.pk, selected_ids)
|
||||
self.assertTrue(all(not (ex.side or '').strip() for ex in selected))
|
||||
selected_ids = [ex.pk for ex in selected]
|
||||
self.assertEqual(
|
||||
set(selected_ids),
|
||||
{left_variant_a.pk, right_variant_a.pk, left_variant_b.pk, right_variant_b.pk},
|
||||
)
|
||||
|
||||
def test_cooldown_excludes_side_specific_variants(self):
|
||||
left_variant = Exercise.objects.create(
|
||||
side_pairs = {}
|
||||
for idx, ex in enumerate(selected):
|
||||
key = selector._strip_side_tokens(ex.name)
|
||||
side_pairs.setdefault(key, []).append(idx)
|
||||
|
||||
self.assertEqual(len(side_pairs['side lying t stretch']), 2)
|
||||
self.assertEqual(len(side_pairs['quadruped adductor stretch with thoracic rotation']), 2)
|
||||
self.assertEqual(
|
||||
side_pairs['side lying t stretch'][1],
|
||||
side_pairs['side lying t stretch'][0] + 1,
|
||||
)
|
||||
self.assertEqual(
|
||||
side_pairs['quadruped adductor stretch with thoracic rotation'][1],
|
||||
side_pairs['quadruped adductor stretch with thoracic rotation'][0] + 1,
|
||||
)
|
||||
|
||||
def test_cooldown_keeps_side_specific_variants_adjacent(self):
|
||||
left_variant_a = Exercise.objects.create(
|
||||
name="Matsyendra's Pose",
|
||||
side='left_side',
|
||||
movement_patterns='static stretch, cool down',
|
||||
@@ -162,7 +178,7 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
right_variant = Exercise.objects.create(
|
||||
right_variant_a = Exercise.objects.create(
|
||||
name="Matsyendra's Pose",
|
||||
side='right_side',
|
||||
movement_patterns='static stretch, cool down',
|
||||
@@ -175,9 +191,9 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
unsided_a = Exercise.objects.create(
|
||||
name='Butterfly Stretch',
|
||||
side='',
|
||||
left_variant_b = Exercise.objects.create(
|
||||
name='Miniband Reverse Clamshell',
|
||||
side='left_leg',
|
||||
movement_patterns='mobility - static, cooldown',
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
@@ -188,25 +204,41 @@ class TestWarmupSelector(TestCase):
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
unsided_b = Exercise.objects.create(
|
||||
name='Hamstring Reach',
|
||||
side='',
|
||||
movement_patterns='static stretch, cool down',
|
||||
right_variant_b = Exercise.objects.create(
|
||||
name='Miniband Reverse Clamshell',
|
||||
side='right_leg',
|
||||
movement_patterns='mobility - static, cooldown',
|
||||
is_duration=True,
|
||||
is_reps=False,
|
||||
is_weight=False,
|
||||
is_compound=False,
|
||||
exercise_tier='accessory',
|
||||
hr_elevation_rating=2,
|
||||
hr_elevation_rating=1,
|
||||
complexity_rating=1,
|
||||
difficulty_level='beginner',
|
||||
)
|
||||
|
||||
selector = ExerciseSelector(self.preference)
|
||||
selected = selector.select_cooldown_exercises(target_muscles=[], count=2)
|
||||
selected_ids = {ex.pk for ex in selected}
|
||||
selected = selector.select_cooldown_exercises(target_muscles=[], count=4)
|
||||
|
||||
self.assertEqual(selected_ids, {unsided_a.pk, unsided_b.pk})
|
||||
self.assertNotIn(left_variant.pk, selected_ids)
|
||||
self.assertNotIn(right_variant.pk, selected_ids)
|
||||
self.assertTrue(all(not (ex.side or '').strip() for ex in selected))
|
||||
selected_ids = [ex.pk for ex in selected]
|
||||
self.assertEqual(
|
||||
set(selected_ids),
|
||||
{left_variant_a.pk, right_variant_a.pk, left_variant_b.pk, right_variant_b.pk},
|
||||
)
|
||||
|
||||
side_pairs = {}
|
||||
for idx, ex in enumerate(selected):
|
||||
key = selector._strip_side_tokens(ex.name)
|
||||
side_pairs.setdefault(key, []).append(idx)
|
||||
|
||||
self.assertEqual(len(side_pairs["matsyendra's pose"]), 2)
|
||||
self.assertEqual(len(side_pairs['miniband reverse clamshell']), 2)
|
||||
self.assertEqual(
|
||||
side_pairs["matsyendra's pose"][1],
|
||||
side_pairs["matsyendra's pose"][0] + 1,
|
||||
)
|
||||
self.assertEqual(
|
||||
side_pairs['miniband reverse clamshell'][1],
|
||||
side_pairs['miniband reverse clamshell'][0] + 1,
|
||||
)
|
||||
|
||||
136
generator/tests/test_workout_generation_modules.py
Normal file
136
generator/tests/test_workout_generation_modules.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from generator.services.workout_generation.entry_rules import (
|
||||
apply_rep_volume_floor,
|
||||
pick_reps_for_exercise,
|
||||
working_rest_seconds,
|
||||
)
|
||||
from generator.services.workout_generation.focus import (
|
||||
focus_key_for_exercise,
|
||||
has_duplicate_focus,
|
||||
)
|
||||
from generator.services.workout_generation.modality import (
|
||||
clamp_duration_bias,
|
||||
plan_superset_modalities,
|
||||
)
|
||||
from generator.services.workout_generation.pattern_planning import (
|
||||
merge_pattern_preferences,
|
||||
rotated_muscle_subset,
|
||||
working_position_label,
|
||||
)
|
||||
from generator.services.workout_generation.recovery import is_recovery_exercise
|
||||
from generator.services.workout_generation.scaling import apply_fitness_scaling
|
||||
from generator.services.workout_generation.section_builders import (
|
||||
build_duration_entries,
|
||||
build_section_superset,
|
||||
section_exercise_count,
|
||||
)
|
||||
|
||||
|
||||
class _Rng:
|
||||
def __init__(self, randint_values=None):
|
||||
self._randint_values = list(randint_values or [])
|
||||
|
||||
def randint(self, low, high):
|
||||
if self._randint_values:
|
||||
return self._randint_values.pop(0)
|
||||
return low
|
||||
|
||||
def shuffle(self, arr):
|
||||
# Deterministic for tests.
|
||||
return None
|
||||
|
||||
|
||||
class _Ex:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
class TestWorkoutGenerationModules(SimpleTestCase):
|
||||
def test_section_count_and_duration_entries(self):
|
||||
rng = _Rng([6, 27, 31])
|
||||
self.assertEqual(section_exercise_count('warmup', 1, rng=rng), 6)
|
||||
|
||||
exercises = [_Ex(name='A'), _Ex(name='B')]
|
||||
entries = build_duration_entries(
|
||||
exercises,
|
||||
duration_min=20,
|
||||
duration_max=40,
|
||||
min_duration=20,
|
||||
duration_multiple=5,
|
||||
rng=rng,
|
||||
)
|
||||
self.assertEqual(entries[0]['duration'], 25)
|
||||
self.assertEqual(entries[1]['duration'], 30)
|
||||
section = build_section_superset('Warm Up', entries)
|
||||
self.assertEqual(section['name'], 'Warm Up')
|
||||
self.assertEqual(section['rounds'], 1)
|
||||
|
||||
def test_scaling_and_rest_floor(self):
|
||||
params = {
|
||||
'rep_min': 4,
|
||||
'rep_max': 10,
|
||||
'rounds': (3, 4),
|
||||
'rest_between_rounds': 60,
|
||||
}
|
||||
scaling = {
|
||||
1: {'rep_min_mult': 1.1, 'rep_max_mult': 1.2, 'rounds_adj': -1, 'rest_adj': 15},
|
||||
2: {'rep_min_mult': 1.0, 'rep_max_mult': 1.0, 'rounds_adj': 0, 'rest_adj': 0},
|
||||
}
|
||||
out = apply_fitness_scaling(
|
||||
params,
|
||||
fitness_level=1,
|
||||
scaling_config=scaling,
|
||||
min_reps=6,
|
||||
min_reps_strength=1,
|
||||
is_strength=True,
|
||||
)
|
||||
self.assertGreaterEqual(out['rep_min'], 5)
|
||||
self.assertEqual(working_rest_seconds(-5, 0), 15)
|
||||
|
||||
def test_modality_helpers(self):
|
||||
self.assertEqual(clamp_duration_bias(0.9, (0.2, 0.6)), 0.6)
|
||||
modalities = plan_superset_modalities(
|
||||
num_supersets=4,
|
||||
duration_bias=0.5,
|
||||
duration_bias_range=(0.25, 0.5),
|
||||
is_strength_workout=False,
|
||||
rng=_Rng(),
|
||||
)
|
||||
self.assertEqual(len(modalities), 4)
|
||||
self.assertTrue(any(modalities))
|
||||
|
||||
def test_pattern_and_focus_helpers(self):
|
||||
self.assertEqual(working_position_label(0, 3), 'early')
|
||||
self.assertEqual(working_position_label(1, 3), 'middle')
|
||||
self.assertEqual(working_position_label(2, 3), 'late')
|
||||
self.assertEqual(
|
||||
merge_pattern_preferences(['upper pull', 'core'], ['core', 'lunge']),
|
||||
['core'],
|
||||
)
|
||||
self.assertEqual(
|
||||
rotated_muscle_subset(['a', 'b', 'c'], 1),
|
||||
['b', 'c', 'a'],
|
||||
)
|
||||
|
||||
curl_a = _Ex(name='Alternating Bicep Curls', movement_patterns='upper pull')
|
||||
curl_b = _Ex(name='Bicep Curls', movement_patterns='upper pull')
|
||||
self.assertEqual(focus_key_for_exercise(curl_a), 'bicep_curl')
|
||||
self.assertTrue(has_duplicate_focus([curl_a, curl_b]))
|
||||
|
||||
def test_recovery_and_rep_selection(self):
|
||||
stretch = _Ex(name='Supine Pec Stretch - T', movement_patterns='mobility - static')
|
||||
self.assertTrue(is_recovery_exercise(stretch))
|
||||
|
||||
ex = _Ex(exercise_tier='primary')
|
||||
reps = pick_reps_for_exercise(
|
||||
ex,
|
||||
{'rep_min': 8, 'rep_max': 12},
|
||||
{'primary': (3, 6)},
|
||||
rng=_Rng([5]),
|
||||
)
|
||||
self.assertEqual(reps, 5)
|
||||
|
||||
entries = [{'reps': 3}, {'duration': 30}]
|
||||
apply_rep_volume_floor(entries, rounds=3, min_volume=12)
|
||||
self.assertEqual(entries[0]['reps'], 4)
|
||||
Reference in New Issue
Block a user