Files
WerkoutAPI/generator/tests/test_movement_enforcement.py
Trey t c80c66c2e5 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>
2026-02-27 22:29:14 -06:00

1101 lines
42 KiB
Python

"""
Tests for _build_working_supersets() — Items #4, #6, #7:
- Movement pattern enforcement (WorkoutStructureRule merging)
- Modality consistency check (duration_bias warning)
- Straight-set strength (first superset = single main lift)
"""
from datetime import date
from django.contrib.auth import get_user_model
from django.test import TestCase
from unittest.mock import patch, MagicMock, PropertyMock
from generator.models import (
MuscleGroupSplit,
MovementPatternOrder,
UserPreference,
WorkoutStructureRule,
WorkoutType,
)
from generator.services.workout_generator import (
FINAL_CONFORMANCE_MAX_RETRIES,
WorkoutGenerator,
STRENGTH_WORKOUT_TYPES,
WORKOUT_TYPE_DEFAULTS,
)
from generator.rules_engine import RuleViolation, validate_workout
from registered_user.models import RegisteredUser
User = get_user_model()
class MovementEnforcementTestBase(TestCase):
"""Shared setup for movement enforcement tests."""
@classmethod
def setUpTestData(cls):
cls.auth_user = User.objects.create_user(
username='testmove', password='testpass123',
)
cls.registered_user = RegisteredUser.objects.create(
first_name='Test', last_name='Move', user=cls.auth_user,
)
# Create workout types
cls.strength_type = WorkoutType.objects.create(
name='traditional strength',
typical_rest_between_sets=120,
typical_intensity='high',
rep_range_min=3,
rep_range_max=6,
duration_bias=0.0,
superset_size_min=1,
superset_size_max=3,
)
cls.hiit_type = WorkoutType.objects.create(
name='hiit',
typical_rest_between_sets=30,
typical_intensity='high',
rep_range_min=10,
rep_range_max=20,
duration_bias=0.7,
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(
name='core_training',
typical_rest_between_sets=30,
typical_intensity='medium',
rep_range_min=10,
rep_range_max=20,
duration_bias=0.5,
superset_size_min=3,
superset_size_max=5,
)
# Create MovementPatternOrder records
MovementPatternOrder.objects.create(
position='early', movement_pattern='lower push - squat',
frequency=20, section_type='working',
)
MovementPatternOrder.objects.create(
position='early', movement_pattern='upper push - horizontal',
frequency=15, section_type='working',
)
MovementPatternOrder.objects.create(
position='middle', movement_pattern='upper pull',
frequency=18, section_type='working',
)
MovementPatternOrder.objects.create(
position='late', movement_pattern='isolation',
frequency=12, section_type='working',
)
# Create WorkoutStructureRule for strength
cls.strength_rule = WorkoutStructureRule.objects.create(
workout_type=cls.strength_type,
section_type='working',
movement_patterns=['lower push - squat', 'hip hinge', 'upper push - horizontal'],
typical_rounds=5,
typical_exercises_per_superset=2,
goal_type='general_fitness',
)
def _make_preference(self, **kwargs):
"""Create a UserPreference for testing."""
defaults = {
'registered_user': self.registered_user,
'days_per_week': 3,
'fitness_level': 2,
'primary_goal': 'general_fitness',
}
defaults.update(kwargs)
return UserPreference.objects.create(**defaults)
def _make_generator(self, pref):
"""Create a WorkoutGenerator with mocked dependencies."""
with patch('generator.services.workout_generator.ExerciseSelector') as MockSelector, \
patch('generator.services.workout_generator.PlanBuilder'):
gen = WorkoutGenerator(pref)
# Make the exercise selector return mock exercises
self.mock_selector = gen.exercise_selector
return gen
def _create_mock_exercise(self, name='Mock Exercise', is_duration=False,
is_weight=True, is_reps=True, is_compound=True,
exercise_tier='primary', movement_patterns='lower push - squat',
hr_elevation_rating=5):
"""Create a mock Exercise object."""
ex = MagicMock()
ex.pk = id(ex) # unique pk
ex.name = name
ex.is_duration = is_duration
ex.is_weight = is_weight
ex.is_reps = is_reps
ex.is_compound = is_compound
ex.exercise_tier = exercise_tier
ex.movement_patterns = movement_patterns
ex.hr_elevation_rating = hr_elevation_rating
ex.side = None
ex.stretch_position = 'mid'
return ex
class TestMovementPatternEnforcement(MovementEnforcementTestBase):
"""Item #4: WorkoutStructureRule patterns merged with position patterns."""
def test_movement_patterns_passed_to_selector(self):
"""select_exercises should receive combined movement pattern preferences
when both position patterns and structure rule patterns exist."""
pref = self._make_preference()
gen = self._make_generator(pref)
# Setup mock exercises
mock_exercises = [
self._create_mock_exercise(f'Exercise {i}')
for i in range(3)
]
gen.exercise_selector.select_exercises.return_value = mock_exercises
gen.exercise_selector.balance_stretch_positions.return_value = mock_exercises
muscle_split = {
'muscles': ['chest', 'back'],
'split_type': 'full_body',
'label': 'Full Body',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
# Verify select_exercises was called
self.assertTrue(gen.exercise_selector.select_exercises.called)
# Check the movement_pattern_preference argument in the first call
first_call_kwargs = gen.exercise_selector.select_exercises.call_args_list[0]
# The call could be positional or keyword - check kwargs
if first_call_kwargs.kwargs.get('movement_pattern_preference') is not None:
patterns = first_call_kwargs.kwargs['movement_pattern_preference']
# Should be combined patterns (intersection of position + rule, or rule[:3])
self.assertIsInstance(patterns, list)
self.assertTrue(len(patterns) > 0)
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()
gen = self._make_generator(pref)
curl_a = self._create_mock_exercise(
'Alternating Bicep Curls',
movement_patterns='upper pull',
is_compound=False,
exercise_tier='accessory',
)
curl_b = self._create_mock_exercise(
'Bicep Curls',
movement_patterns='upper pull',
is_compound=False,
exercise_tier='accessory',
)
pull = self._create_mock_exercise('Bent Over Row', movement_patterns='upper pull')
hinge = self._create_mock_exercise('Romanian Deadlift', movement_patterns='hip hinge')
gen.exercise_selector.select_exercises.side_effect = [
[curl_a, curl_b], # rejected: duplicate focus
[pull, hinge], # accepted
]
gen.exercise_selector.balance_stretch_positions.side_effect = lambda exs, **_: exs
muscle_split = {
'muscles': ['upper back', 'biceps'],
'split_type': 'pull',
'label': 'Pull',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['hiit'])
wt_params['num_supersets'] = (1, 1)
wt_params['exercises_per_superset'] = (2, 2)
wt_params['duration_bias'] = 0.0
supersets = gen._build_working_supersets(muscle_split, self.hiit_type, wt_params)
self.assertEqual(len(supersets), 1)
self.assertGreaterEqual(gen.exercise_selector.select_exercises.call_count, 2)
names = [
entry['exercise'].name
for entry in supersets[0].get('exercises', [])
]
self.assertNotEqual(
set(names),
{'Alternating Bicep Curls', 'Bicep Curls'},
f'Expected duplicate-focus superset to be retried, got {names}',
)
pref.delete()
class TestStrengthStraightSets(MovementEnforcementTestBase):
"""Item #7: First working superset in strength = single main lift."""
def test_strength_first_superset_single_exercise(self):
"""For traditional strength, the first working superset should request
exactly 1 exercise (straight set of a main lift)."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_ex = self._create_mock_exercise('Barbell Squat')
gen.exercise_selector.select_exercises.return_value = [mock_ex]
gen.exercise_selector.balance_stretch_positions.return_value = [mock_ex]
muscle_split = {
'muscles': ['quads', 'hamstrings'],
'split_type': 'lower',
'label': 'Lower',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
self.assertGreaterEqual(len(supersets), 1)
# First superset should have been requested with count=1
first_call = gen.exercise_selector.select_exercises.call_args_list[0]
self.assertEqual(first_call.kwargs.get('count', first_call.args[1] if len(first_call.args) > 1 else None), 1)
pref.delete()
def test_strength_first_superset_more_rounds(self):
"""First superset of a strength workout should have 4-6 rounds."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_ex = self._create_mock_exercise('Deadlift')
gen.exercise_selector.select_exercises.return_value = [mock_ex]
gen.exercise_selector.balance_stretch_positions.return_value = [mock_ex]
muscle_split = {
'muscles': ['hamstrings', 'glutes'],
'split_type': 'lower',
'label': 'Lower',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
# Run multiple times to check round ranges
round_counts = set()
for _ in range(50):
gen.exercise_selector.select_exercises.return_value = [mock_ex]
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
if supersets:
round_counts.add(supersets[0]['rounds'])
# All first-superset round counts should be in [4, 6]
for r in round_counts:
self.assertGreaterEqual(r, 4, f"Rounds {r} below minimum 4")
self.assertLessEqual(r, 6, f"Rounds {r} above maximum 6")
pref.delete()
def test_strength_first_superset_rest_period(self):
"""First superset of a strength workout should use the workout type's
typical_rest_between_sets for rest."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_ex = self._create_mock_exercise('Bench Press')
gen.exercise_selector.select_exercises.return_value = [mock_ex]
gen.exercise_selector.balance_stretch_positions.return_value = [mock_ex]
muscle_split = {
'muscles': ['chest', 'triceps'],
'split_type': 'push',
'label': 'Push',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
if supersets:
# typical_rest_between_sets for our strength_type is 120
self.assertEqual(supersets[0]['rest_between_rounds'], 120)
pref.delete()
def test_strength_accessories_still_superset(self):
"""2nd+ supersets in strength workouts should still have 2+ exercises
(the min_ex_per_ss rule still applies to non-first supersets)."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_exercises = [
self._create_mock_exercise(f'Accessory {i}', exercise_tier='accessory')
for i in range(3)
]
gen.exercise_selector.select_exercises.return_value = mock_exercises
gen.exercise_selector.balance_stretch_positions.return_value = mock_exercises
muscle_split = {
'muscles': ['chest', 'back', 'shoulders'],
'split_type': 'upper',
'label': 'Upper',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
# Should have multiple supersets
if len(supersets) >= 2:
# Retries may add extra calls; assert at least one non-first
# working-superset request asks for 2+ exercises.
observed_counts = []
for call in gen.exercise_selector.select_exercises.call_args_list:
count_arg = call.kwargs.get('count')
if count_arg is None and len(call.args) > 1:
count_arg = call.args[1]
if count_arg is not None:
observed_counts.append(count_arg)
self.assertTrue(
any(c >= 2 for c in observed_counts),
f"Expected at least one accessory superset request >=2 exercises, got {observed_counts}",
)
pref.delete()
def test_non_strength_no_single_exercise_override(self):
"""Non-strength workouts should NOT have the single-exercise first superset."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_exercises = [
self._create_mock_exercise(f'HIIT Move {i}', is_duration=True, is_weight=False)
for i in range(5)
]
gen.exercise_selector.select_exercises.return_value = mock_exercises
gen.exercise_selector.balance_stretch_positions.return_value = mock_exercises
muscle_split = {
'muscles': ['chest', 'back', 'quads'],
'split_type': 'full_body',
'label': 'Full Body',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['hiit'])
supersets = gen._build_working_supersets(
muscle_split, self.hiit_type, wt_params,
)
# First call to select_exercises should NOT have count=1
first_call = gen.exercise_selector.select_exercises.call_args_list[0]
count_arg = first_call.kwargs.get('count')
if count_arg is None and len(first_call.args) > 1:
count_arg = first_call.args[1]
self.assertGreater(count_arg, 1, "Non-strength first superset should have > 1 exercise")
pref.delete()
def test_strength_first_superset_survives_post_processing(self):
"""generate_single_workout should preserve first strength straight set."""
pref = self._make_preference(primary_goal='strength')
gen = self._make_generator(pref)
main_lift = self._create_mock_exercise('Back Squat', exercise_tier='primary')
accessory_1 = self._create_mock_exercise('DB Row', exercise_tier='secondary')
accessory_2 = self._create_mock_exercise('RDL', exercise_tier='secondary')
accessory_3 = self._create_mock_exercise('Lat Pulldown', exercise_tier='accessory')
gen._build_warmup = MagicMock(return_value=None)
gen._build_cooldown = MagicMock(return_value=None)
gen._check_quality_gates = MagicMock(return_value=[])
gen._get_final_conformance_violations = MagicMock(return_value=[])
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
gen._build_working_supersets = MagicMock(return_value=[
{
'name': 'Working Set 1',
'rounds': 5,
'rest_between_rounds': 120,
'modality': 'reps',
'exercises': [
{'exercise': main_lift, 'reps': 5, 'order': 1},
],
},
{
'name': 'Working Set 2',
'rounds': 3,
'rest_between_rounds': 90,
'modality': 'reps',
'exercises': [
{'exercise': accessory_1, 'reps': 10, 'order': 1},
{'exercise': accessory_2, 'reps': 10, 'order': 2},
{'exercise': accessory_3, 'reps': 12, 'order': 3},
],
},
])
muscle_split = {
'muscles': ['quads', 'hamstrings'],
'split_type': 'lower',
'label': 'Lower',
}
workout_spec = gen.generate_single_workout(
muscle_split=muscle_split,
workout_type=self.strength_type,
scheduled_date=date(2026, 3, 2),
)
working = [
ss for ss in workout_spec.get('supersets', [])
if ss.get('name', '').startswith('Working')
]
self.assertGreaterEqual(len(working), 1)
self.assertEqual(
len(working[0].get('exercises', [])),
1,
f'Expected first strength working set to stay at 1 exercise, got: {working[0]}',
)
pref.delete()
class TestModalityConsistency(MovementEnforcementTestBase):
"""Item #6: Modality consistency warning for duration-dominant workouts."""
def test_duration_dominant_warns_on_low_ratio(self):
"""When duration_bias >= 0.6 and most exercises are rep-based,
a warning should be appended to self.warnings."""
pref = self._make_preference()
gen = self._make_generator(pref)
# Create mostly rep-based (non-duration) exercises
mock_exercises = [
self._create_mock_exercise(f'Rep Exercise {i}', is_duration=False)
for i in range(4)
]
gen.exercise_selector.select_exercises.return_value = mock_exercises
gen.exercise_selector.balance_stretch_positions.return_value = mock_exercises
muscle_split = {
'muscles': ['chest', 'back'],
'split_type': 'full_body',
'label': 'Full Body',
}
# Use HIIT params (duration_bias = 0.7 >= 0.6)
wt_params = dict(WORKOUT_TYPE_DEFAULTS['hiit'])
# But force rep-based by setting duration_bias low in actual randomization
# We need to make superset_is_duration = False for all supersets
# Override duration_bias to be very low so random.random() > it
# But wt_params['duration_bias'] stays at 0.7 for the post-check
# Actually, the modality check uses wt_params['duration_bias'] which is 0.7
# The rep-based exercises come from select_exercises mock returning
# exercises with is_duration=False
supersets = gen._build_working_supersets(
muscle_split, self.hiit_type, wt_params,
)
# The exercises are not is_duration so the check should fire
# Look for the modality mismatch warning
modality_warnings = [
w for w in gen.warnings if 'Modality mismatch' in w
]
# Note: This depends on whether the supersets ended up rep-based
# Since duration_bias is 0.7, most supersets will be duration-based
# and our mock exercises don't have is_duration=True, so they'd be
# skipped in the duration superset builder (the continue clause).
# The modality check counts exercises that ARE is_duration.
# With is_duration=False mocks in duration supersets, they'd be skipped.
# So total_exercises could be 0 (if all were skipped).
# Let's verify differently: the test should check the logic directly.
# Create a scenario where the duration check definitely triggers:
# Set duration_bias high but exercises are rep-based
gen.warnings = [] # Reset warnings
# Create supersets manually to test the post-check
# Simulate: wt_params has high duration_bias, but exercises are rep-based
wt_params_high_dur = dict(wt_params)
wt_params_high_dur['duration_bias'] = 0.8
# Return exercises that won't be skipped (rep-based supersets with non-duration exercises)
rep_exercises = [
self._create_mock_exercise(f'Rep Ex {i}', is_duration=False)
for i in range(3)
]
gen.exercise_selector.select_exercises.return_value = rep_exercises
gen.exercise_selector.balance_stretch_positions.return_value = rep_exercises
# Force non-strength workout type with low actual random for duration
# Use a non-strength type so is_strength_workout is False
# Create a real WorkoutType to avoid MagicMock pk issues with Django ORM
non_strength_type = WorkoutType.objects.create(
name='circuit training',
typical_rest_between_sets=30,
duration_bias=0.7,
)
# Patch random to make all supersets rep-based despite high duration_bias
import random
original_random = random.random
random.random = lambda: 0.99 # Always > duration_bias, so rep-based
try:
supersets = gen._build_working_supersets(
muscle_split, non_strength_type, wt_params_high_dur,
)
finally:
random.random = original_random
# Now check warnings
modality_warnings = [
w for w in gen.warnings if 'Modality mismatch' in w
]
if supersets and any(ss.get('exercises') for ss in supersets):
self.assertTrue(
len(modality_warnings) > 0,
f"Expected modality mismatch warning but got: {gen.warnings}",
)
pref.delete()
def test_no_warning_when_duration_bias_low(self):
"""When duration_bias < 0.6, no modality consistency warning
should be emitted even if exercises are all rep-based."""
pref = self._make_preference()
gen = self._make_generator(pref)
mock_exercises = [
self._create_mock_exercise(f'Rep Exercise {i}', is_duration=False)
for i in range(3)
]
gen.exercise_selector.select_exercises.return_value = mock_exercises
gen.exercise_selector.balance_stretch_positions.return_value = mock_exercises
muscle_split = {
'muscles': ['chest', 'back'],
'split_type': 'full_body',
'label': 'Full Body',
}
# Use strength params (duration_bias = 0.0 < 0.6)
wt_params = dict(WORKOUT_TYPE_DEFAULTS['traditional strength'])
supersets = gen._build_working_supersets(
muscle_split, self.strength_type, wt_params,
)
modality_warnings = [
w for w in gen.warnings if 'Modality mismatch' in w
]
self.assertEqual(
len(modality_warnings), 0,
f"Should not have modality warning for low duration_bias but got: {modality_warnings}",
)
pref.delete()
def test_no_warning_when_duration_ratio_sufficient(self):
"""When duration_bias >= 0.6 and duration exercises >= 50%,
no warning should be emitted."""
pref = self._make_preference()
gen = self._make_generator(pref)
# Create mostly duration exercises
duration_exercises = [
self._create_mock_exercise(f'Duration Ex {i}', is_duration=True, is_weight=False)
for i in range(4)
]
gen.exercise_selector.select_exercises.return_value = duration_exercises
gen.exercise_selector.balance_stretch_positions.return_value = duration_exercises
muscle_split = {
'muscles': ['chest', 'back'],
'split_type': 'full_body',
'label': 'Full Body',
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['hiit'])
supersets = gen._build_working_supersets(
muscle_split, self.hiit_type, wt_params,
)
modality_warnings = [
w for w in gen.warnings if 'Modality mismatch' in w
]
self.assertEqual(
len(modality_warnings), 0,
f"Should not have modality warning when duration ratio is sufficient but got: {modality_warnings}",
)
pref.delete()
class TestFinalConformance(MovementEnforcementTestBase):
"""Strict final conformance enforcement for assembled workouts."""
def test_core_workout_respects_type_max_exercise_cap(self):
"""Core workouts should be trimmed to the calibrated max (8 working exercises)."""
pref = self._make_preference(primary_goal='general_fitness')
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._get_final_conformance_violations = MagicMock(return_value=[])
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
working_exercises = [
{'exercise': self._create_mock_exercise(f'Core Push {i}', movement_patterns='upper push, core'), 'reps': 12, 'order': i + 1}
for i in range(6)
]
more_working_exercises = [
{'exercise': self._create_mock_exercise(f'Core Pull {i}', movement_patterns='upper pull, core'), 'reps': 12, 'order': i + 1}
for i in range(6)
]
gen._build_working_supersets = MagicMock(return_value=[
{
'name': 'Working Set 1',
'rounds': 3,
'rest_between_rounds': 30,
'modality': 'reps',
'exercises': working_exercises,
},
{
'name': 'Working Set 2',
'rounds': 3,
'rest_between_rounds': 30,
'modality': 'reps',
'exercises': more_working_exercises,
},
])
workout_spec = gen.generate_single_workout(
muscle_split={
'muscles': ['core', 'abs', 'obliques'],
'split_type': 'core',
'label': 'Core Day',
},
workout_type=self.core_type,
scheduled_date=date(2026, 3, 2),
)
working = [
ss for ss in workout_spec.get('supersets', [])
if ss.get('name', '').startswith('Working')
]
total_working = sum(len(ss.get('exercises', [])) for ss in working)
self.assertLessEqual(
total_working, 8,
f'Expected core workout to cap at 8 working exercises, got {total_working}',
)
pref.delete()
def test_core_cap_removes_extra_minimum_supersets(self):
"""When all sets are already at minimum size, remove trailing sets to hit cap."""
pref = self._make_preference(primary_goal='general_fitness')
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._get_final_conformance_violations = MagicMock(return_value=[])
gen._adjust_to_time_target = MagicMock(side_effect=lambda spec, *_args, **_kwargs: spec)
working_supersets = []
for idx in range(6):
push = self._create_mock_exercise(
f'Push {idx}',
movement_patterns='upper push',
)
pull = self._create_mock_exercise(
f'Pull {idx}',
movement_patterns='upper pull',
)
working_supersets.append({
'name': f'Working Set {idx + 1}',
'rounds': 3,
'rest_between_rounds': 30,
'modality': 'reps',
'exercises': [
{'exercise': push, 'reps': 12, 'order': 1},
{'exercise': pull, 'reps': 12, 'order': 2},
],
})
gen._build_working_supersets = MagicMock(return_value=working_supersets)
workout_spec = gen.generate_single_workout(
muscle_split={
'muscles': ['core', 'abs', 'obliques'],
'split_type': 'core',
'label': 'Core Day',
},
workout_type=self.core_type,
scheduled_date=date(2026, 3, 2),
)
working = [
ss for ss in workout_spec.get('supersets', [])
if ss.get('name', '').startswith('Working')
]
total_working = sum(len(ss.get('exercises', [])) for ss in working)
self.assertLessEqual(total_working, 8)
self.assertLessEqual(len(working), 4)
pref.delete()
def test_pad_to_fill_respects_type_cap(self):
"""Padding should stop when workout-type max working-exercise cap is reached."""
pref = self._make_preference(primary_goal='general_fitness')
gen = self._make_generator(pref)
gen._estimate_total_time = MagicMock(return_value=0)
gen.exercise_selector.select_exercises.return_value = [
self._create_mock_exercise('Pad Exercise', movement_patterns='upper pull')
]
base_ex_a = self._create_mock_exercise('Base A', movement_patterns='upper push')
base_ex_b = self._create_mock_exercise('Base B', movement_patterns='upper pull')
workout_spec = {
'supersets': [
{
'name': 'Working Set 1',
'rounds': 3,
'rest_between_rounds': 30,
'modality': 'reps',
'exercises': [
{'exercise': base_ex_a, 'reps': 12, 'order': 1},
{'exercise': base_ex_b, 'reps': 12, 'order': 2},
{'exercise': base_ex_a, 'reps': 12, 'order': 3},
],
},
{
'name': 'Working Set 2',
'rounds': 3,
'rest_between_rounds': 30,
'modality': 'reps',
'exercises': [
{'exercise': base_ex_b, 'reps': 12, 'order': 1},
{'exercise': base_ex_a, 'reps': 12, 'order': 2},
{'exercise': base_ex_b, 'reps': 12, 'order': 3},
],
},
],
}
wt_params = dict(WORKOUT_TYPE_DEFAULTS['core'])
wt_params['duration_bias'] = 0.0
padded = gen._pad_to_fill(
workout_spec=workout_spec,
max_duration_sec=3600,
muscle_split={
'muscles': ['core', 'abs'],
'split_type': 'core',
'label': 'Core Day',
},
wt_params=wt_params,
workout_type=self.core_type,
)
total_working = sum(
len(ss.get('exercises', []))
for ss in padded.get('supersets', [])
if ss.get('name', '').startswith('Working')
)
self.assertLessEqual(total_working, 8)
pref.delete()
def test_compound_ordering_uses_validator_definition(self):
"""Accessory-tagged entries should not be treated as compounds in ordering."""
pref = self._make_preference(primary_goal='general_fitness')
gen = self._make_generator(pref)
accessory_flagged_compound = self._create_mock_exercise(
'Accessory Marked Compound',
is_compound=True,
exercise_tier='accessory',
movement_patterns='upper push',
)
true_compound = self._create_mock_exercise(
'Primary Compound',
is_compound=True,
exercise_tier='secondary',
movement_patterns='upper pull',
)
workout_spec = {
'supersets': [
{
'name': 'Working Set 1',
'rounds': 3,
'rest_between_rounds': 45,
'modality': 'reps',
'exercises': [
{'exercise': accessory_flagged_compound, 'reps': 10, 'order': 1},
{'exercise': true_compound, 'reps': 8, 'order': 2},
],
},
],
}
gen._enforce_compound_first_order(workout_spec, is_strength_workout=False)
violations = validate_workout(workout_spec, 'hiit', 'general_fitness')
compound_order_violations = [
v for v in violations
if v.rule_id == 'compound_before_isolation'
]
self.assertEqual(len(compound_order_violations), 0)
pref.delete()
def test_final_warning_triggers_regeneration(self):
"""A final warning should trigger full regeneration before returning."""
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('Balanced Pull', movement_patterns='upper pull')
gen._build_working_supersets = MagicMock(return_value=[
{
'name': 'Working Set 1',
'rounds': 3,
'rest_between_rounds': 45,
'modality': 'reps',
'exercises': [{'exercise': ex, 'reps': 10, 'order': 1}],
},
])
gen._get_final_conformance_violations = MagicMock(side_effect=[
[RuleViolation(
rule_id='exercise_count_cap',
severity='warning',
message='Too many exercises',
)],
[],
])
gen.generate_single_workout(
muscle_split={
'muscles': ['upper back', 'lats'],
'split_type': 'pull',
'label': 'Pull Day',
},
workout_type=self.hiit_type,
scheduled_date=date(2026, 3, 3),
)
self.assertEqual(
gen._build_working_supersets.call_count, 2,
'Expected regeneration after final warning.',
)
pref.delete()
def test_unresolved_final_violations_raise_error(self):
"""Generator should fail fast when conformance cannot be achieved."""
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('Push Only', movement_patterns='upper push')
gen._build_working_supersets = MagicMock(return_value=[
{
'name': 'Working Set 1',
'rounds': 3,
'rest_between_rounds': 45,
'modality': 'reps',
'exercises': [{'exercise': ex, 'reps': 10, 'order': 1}],
},
])
gen._get_final_conformance_violations = MagicMock(return_value=[
RuleViolation(
rule_id='push_pull_ratio',
severity='warning',
message='Pull:push ratio too low',
),
])
with self.assertRaises(ValueError):
gen.generate_single_workout(
muscle_split={
'muscles': ['chest', 'triceps'],
'split_type': 'push',
'label': 'Push Day',
},
workout_type=self.hiit_type,
scheduled_date=date(2026, 3, 4),
)
self.assertEqual(
gen._build_working_supersets.call_count,
FINAL_CONFORMANCE_MAX_RETRIES + 1,
)
pref.delete()
def test_info_violation_is_not_blocking(self):
"""Info-level rules should not fail generation in strict mode."""
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')
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=[
RuleViolation(
rule_id='compound_before_isolation',
severity='info',
message='Compound exercises should generally appear before isolation.',
),
])
workout = 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, 5),
)
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()