- Add Next.js rewrites to proxy API calls through same origin (fixes login/media on werkout.treytartt.com) - Fix mediaUrl() in DayCard and ExerciseRow to use relative paths in production - Add proxyTimeout for long-running workout generation endpoints - Add CSRF trusted origin for treytartt.com - Split docker-compose into production (Unraid) and dev configs - Show display_name and descriptions on workout type cards - Generator: rules engine improvements, movement enforcement, exercise selector updates - Add new test files for rules drift, workout research generation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
701 lines
25 KiB
Python
701 lines
25 KiB
Python
"""
|
|
Tests for the rules engine: WORKOUT_TYPE_RULES coverage,
|
|
validate_workout() error/warning detection, and quality gate retry logic.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
from django.test import TestCase
|
|
|
|
from generator.rules_engine import (
|
|
validate_workout,
|
|
RuleViolation,
|
|
WORKOUT_TYPE_RULES,
|
|
UNIVERSAL_RULES,
|
|
DB_CALIBRATION,
|
|
_normalize_type_key,
|
|
_classify_rep_weight,
|
|
_has_warmup,
|
|
_has_cooldown,
|
|
_get_working_supersets,
|
|
_count_push_pull,
|
|
_check_compound_before_isolation,
|
|
)
|
|
|
|
|
|
def _make_exercise(**kwargs):
|
|
"""Create a mock exercise object with the given attributes."""
|
|
defaults = {
|
|
'exercise_tier': 'accessory',
|
|
'is_reps': True,
|
|
'is_compound': False,
|
|
'is_weight': False,
|
|
'is_duration': False,
|
|
'movement_patterns': '',
|
|
'name': 'Test Exercise',
|
|
'stretch_position': None,
|
|
'difficulty_level': 'intermediate',
|
|
'complexity_rating': 3,
|
|
'hr_elevation_rating': 5,
|
|
'estimated_rep_duration': 3.0,
|
|
}
|
|
defaults.update(kwargs)
|
|
ex = MagicMock()
|
|
for k, v in defaults.items():
|
|
setattr(ex, k, v)
|
|
return ex
|
|
|
|
|
|
def _make_entry(exercise=None, reps=None, duration=None, order=1):
|
|
"""Create an exercise entry dict for a superset."""
|
|
entry = {'order': order}
|
|
entry['exercise'] = exercise or _make_exercise()
|
|
if reps is not None:
|
|
entry['reps'] = reps
|
|
if duration is not None:
|
|
entry['duration'] = duration
|
|
return entry
|
|
|
|
|
|
def _make_superset(name='Working Set 1', exercises=None, rounds=3):
|
|
"""Create a superset dict."""
|
|
return {
|
|
'name': name,
|
|
'exercises': exercises or [],
|
|
'rounds': rounds,
|
|
}
|
|
|
|
|
|
class TestWorkoutTypeRulesCoverage(TestCase):
|
|
"""Verify that WORKOUT_TYPE_RULES covers all 8 workout types."""
|
|
|
|
def test_all_8_workout_types_have_rules(self):
|
|
expected_types = [
|
|
'traditional_strength_training',
|
|
'hypertrophy',
|
|
'high_intensity_interval_training',
|
|
'functional_strength_training',
|
|
'cross_training',
|
|
'core_training',
|
|
'flexibility',
|
|
'cardio',
|
|
]
|
|
for wt in expected_types:
|
|
self.assertIn(wt, WORKOUT_TYPE_RULES, f"Missing rules for {wt}")
|
|
|
|
def test_each_type_has_required_keys(self):
|
|
required_keys = [
|
|
'rep_ranges', 'rest_periods', 'duration_bias_range',
|
|
'superset_size_range', 'round_range', 'typical_rest',
|
|
'typical_intensity',
|
|
]
|
|
for wt_name, rules in WORKOUT_TYPE_RULES.items():
|
|
for key in required_keys:
|
|
self.assertIn(
|
|
key, rules,
|
|
f"Missing key '{key}' in rules for {wt_name}",
|
|
)
|
|
|
|
def test_rep_ranges_have_all_tiers(self):
|
|
for wt_name, rules in WORKOUT_TYPE_RULES.items():
|
|
rep_ranges = rules['rep_ranges']
|
|
for tier in ('primary', 'secondary', 'accessory'):
|
|
self.assertIn(
|
|
tier, rep_ranges,
|
|
f"Missing rep range tier '{tier}' in {wt_name}",
|
|
)
|
|
low, high = rep_ranges[tier]
|
|
self.assertLessEqual(
|
|
low, high,
|
|
f"Invalid rep range ({low}, {high}) for {tier} in {wt_name}",
|
|
)
|
|
|
|
|
|
class TestDBCalibrationCoverage(TestCase):
|
|
"""Verify DB_CALIBRATION has entries for all 8 types."""
|
|
|
|
def test_all_8_types_in_calibration(self):
|
|
expected_names = [
|
|
'functional_strength_training',
|
|
'traditional_strength_training',
|
|
'high_intensity_interval_training',
|
|
'cross_training',
|
|
'core_training',
|
|
'flexibility',
|
|
'cardio',
|
|
'hypertrophy',
|
|
]
|
|
for name in expected_names:
|
|
self.assertIn(name, DB_CALIBRATION, f"Missing {name} in DB_CALIBRATION")
|
|
|
|
|
|
class TestHelperFunctions(TestCase):
|
|
"""Test utility functions used by validate_workout."""
|
|
|
|
def test_normalize_type_key(self):
|
|
self.assertEqual(
|
|
_normalize_type_key('Traditional Strength Training'),
|
|
'traditional_strength_training',
|
|
)
|
|
self.assertEqual(_normalize_type_key('HIIT'), 'high_intensity_interval_training')
|
|
self.assertEqual(
|
|
_normalize_type_key('high intensity interval training'),
|
|
'high_intensity_interval_training',
|
|
)
|
|
self.assertEqual(_normalize_type_key('cardio'), 'cardio')
|
|
|
|
def test_classify_rep_weight(self):
|
|
self.assertEqual(_classify_rep_weight(3), 'heavy')
|
|
self.assertEqual(_classify_rep_weight(5), 'heavy')
|
|
self.assertEqual(_classify_rep_weight(8), 'moderate')
|
|
self.assertEqual(_classify_rep_weight(12), 'light')
|
|
|
|
def test_has_warmup(self):
|
|
supersets = [
|
|
_make_superset(name='Warm Up'),
|
|
_make_superset(name='Working Set 1'),
|
|
]
|
|
self.assertTrue(_has_warmup(supersets))
|
|
self.assertFalse(_has_warmup([_make_superset(name='Working Set 1')]))
|
|
|
|
def test_has_cooldown(self):
|
|
supersets = [
|
|
_make_superset(name='Working Set 1'),
|
|
_make_superset(name='Cool Down'),
|
|
]
|
|
self.assertTrue(_has_cooldown(supersets))
|
|
self.assertFalse(_has_cooldown([_make_superset(name='Working Set 1')]))
|
|
|
|
def test_get_working_supersets(self):
|
|
supersets = [
|
|
_make_superset(name='Warm Up'),
|
|
_make_superset(name='Working Set 1'),
|
|
_make_superset(name='Working Set 2'),
|
|
_make_superset(name='Cool Down'),
|
|
]
|
|
working = _get_working_supersets(supersets)
|
|
self.assertEqual(len(working), 2)
|
|
self.assertEqual(working[0]['name'], 'Working Set 1')
|
|
|
|
def test_count_push_pull(self):
|
|
push_ex = _make_exercise(movement_patterns='upper push')
|
|
pull_ex = _make_exercise(movement_patterns='upper pull')
|
|
supersets = [
|
|
_make_superset(
|
|
name='Working Set 1',
|
|
exercises=[
|
|
_make_entry(exercise=push_ex, reps=8),
|
|
_make_entry(exercise=pull_ex, reps=8),
|
|
],
|
|
),
|
|
]
|
|
push_count, pull_count = _count_push_pull(supersets)
|
|
self.assertEqual(push_count, 1)
|
|
self.assertEqual(pull_count, 1)
|
|
|
|
def test_compound_before_isolation_correct(self):
|
|
compound = _make_exercise(is_compound=True, exercise_tier='primary')
|
|
isolation = _make_exercise(is_compound=False, exercise_tier='accessory')
|
|
supersets = [
|
|
_make_superset(
|
|
name='Working Set 1',
|
|
exercises=[
|
|
_make_entry(exercise=compound, reps=5, order=1),
|
|
_make_entry(exercise=isolation, reps=12, order=2),
|
|
],
|
|
),
|
|
]
|
|
self.assertTrue(_check_compound_before_isolation(supersets))
|
|
|
|
def test_compound_before_isolation_violated(self):
|
|
compound = _make_exercise(is_compound=True, exercise_tier='primary')
|
|
isolation = _make_exercise(is_compound=False, exercise_tier='accessory')
|
|
supersets = [
|
|
_make_superset(
|
|
name='Working Set 1',
|
|
exercises=[
|
|
_make_entry(exercise=isolation, reps=12, order=1),
|
|
],
|
|
),
|
|
_make_superset(
|
|
name='Working Set 2',
|
|
exercises=[
|
|
_make_entry(exercise=compound, reps=5, order=1),
|
|
],
|
|
),
|
|
]
|
|
self.assertFalse(_check_compound_before_isolation(supersets))
|
|
|
|
|
|
class TestValidateWorkout(TestCase):
|
|
"""Test the main validate_workout function."""
|
|
|
|
def test_empty_workout_produces_error(self):
|
|
violations = validate_workout({'supersets': []}, 'hiit', 'general_fitness')
|
|
errors = [v for v in violations if v.severity == 'error']
|
|
self.assertTrue(len(errors) > 0)
|
|
self.assertEqual(errors[0].rule_id, 'empty_workout')
|
|
|
|
def test_validate_catches_rep_range_violation(self):
|
|
"""Strength workout with reps=20 on primary should produce error."""
|
|
workout_spec = {
|
|
'supersets': [
|
|
_make_superset(
|
|
name='Working Set 1',
|
|
exercises=[
|
|
_make_entry(
|
|
exercise=_make_exercise(
|
|
exercise_tier='primary',
|
|
is_reps=True,
|
|
),
|
|
reps=20,
|
|
),
|
|
],
|
|
rounds=3,
|
|
),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'traditional_strength_training', 'strength',
|
|
)
|
|
rep_errors = [
|
|
v for v in violations
|
|
if v.severity == 'error' and 'rep_range' in v.rule_id
|
|
]
|
|
self.assertTrue(
|
|
len(rep_errors) > 0,
|
|
f"Expected rep range error, got: {[v.rule_id for v in violations]}",
|
|
)
|
|
|
|
def test_validate_passes_valid_strength_workout(self):
|
|
"""A well-formed strength workout with warmup + working + cooldown."""
|
|
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=_make_exercise(
|
|
exercise_tier='primary',
|
|
is_reps=True,
|
|
is_compound=True,
|
|
is_weight=True,
|
|
movement_patterns='upper push',
|
|
),
|
|
reps=5,
|
|
),
|
|
],
|
|
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, 'traditional_strength_training', 'strength',
|
|
)
|
|
errors = [v for v in violations if v.severity == 'error']
|
|
self.assertEqual(
|
|
len(errors), 0,
|
|
f"Unexpected errors: {[v.message for v in errors]}",
|
|
)
|
|
|
|
def test_warmup_missing_produces_error(self):
|
|
"""Workout without warmup should produce an error."""
|
|
workout_spec = {
|
|
'supersets': [
|
|
_make_superset(
|
|
name='Working Set 1',
|
|
exercises=[
|
|
_make_entry(
|
|
exercise=_make_exercise(
|
|
exercise_tier='primary',
|
|
is_reps=True,
|
|
is_compound=True,
|
|
is_weight=True,
|
|
),
|
|
reps=5,
|
|
),
|
|
],
|
|
rounds=4,
|
|
),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'traditional_strength_training', 'strength',
|
|
)
|
|
warmup_errors = [
|
|
v for v in violations
|
|
if v.rule_id == 'warmup_missing'
|
|
]
|
|
self.assertEqual(len(warmup_errors), 1)
|
|
|
|
def test_cooldown_missing_produces_warning(self):
|
|
"""Workout without cooldown should produce a warning."""
|
|
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=_make_exercise(
|
|
exercise_tier='primary',
|
|
is_reps=True,
|
|
is_compound=True,
|
|
is_weight=True,
|
|
),
|
|
reps=5,
|
|
),
|
|
],
|
|
rounds=4,
|
|
),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'traditional_strength_training', 'strength',
|
|
)
|
|
cooldown_warnings = [
|
|
v for v in violations
|
|
if v.rule_id == 'cooldown_missing'
|
|
]
|
|
self.assertEqual(len(cooldown_warnings), 1)
|
|
self.assertEqual(cooldown_warnings[0].severity, 'warning')
|
|
|
|
def test_push_pull_ratio_enforcement(self):
|
|
"""All push, no pull -> warning."""
|
|
push_exercises = [
|
|
_make_entry(
|
|
exercise=_make_exercise(
|
|
movement_patterns='upper push',
|
|
is_compound=True,
|
|
is_weight=True,
|
|
exercise_tier='primary',
|
|
),
|
|
reps=8,
|
|
order=i + 1,
|
|
)
|
|
for i in range(4)
|
|
]
|
|
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=push_exercises,
|
|
rounds=3,
|
|
),
|
|
_make_superset(name='Cool Down', exercises=[
|
|
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
|
], rounds=1),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'hypertrophy', 'hypertrophy',
|
|
)
|
|
ratio_violations = [v for v in violations if v.rule_id == 'push_pull_ratio']
|
|
self.assertTrue(
|
|
len(ratio_violations) > 0,
|
|
"Expected push:pull ratio warning for all-push workout",
|
|
)
|
|
|
|
def test_workout_type_match_violation(self):
|
|
"""Non-strength exercises in a strength workout should trigger match violation."""
|
|
# All duration-based, non-compound, non-weight exercises for strength
|
|
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=_make_exercise(
|
|
exercise_tier='accessory',
|
|
is_reps=True,
|
|
is_compound=False,
|
|
is_weight=False,
|
|
),
|
|
reps=15,
|
|
)
|
|
for _ in range(5)
|
|
],
|
|
rounds=3,
|
|
),
|
|
_make_superset(name='Cool Down', exercises=[
|
|
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
|
], rounds=1),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'traditional_strength_training', 'strength',
|
|
)
|
|
match_violations = [
|
|
v for v in violations
|
|
if v.rule_id == 'workout_type_match'
|
|
]
|
|
self.assertTrue(
|
|
len(match_violations) > 0,
|
|
"Expected workout type match violation for non-strength exercises",
|
|
)
|
|
|
|
def test_superset_size_warning(self):
|
|
"""Traditional strength with >5 exercises per superset should warn."""
|
|
many_exercises = [
|
|
_make_entry(
|
|
exercise=_make_exercise(
|
|
exercise_tier='accessory',
|
|
is_reps=True,
|
|
is_weight=True,
|
|
is_compound=True,
|
|
),
|
|
reps=5,
|
|
order=i + 1,
|
|
)
|
|
for i in range(8)
|
|
]
|
|
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=many_exercises,
|
|
rounds=3,
|
|
),
|
|
_make_superset(name='Cool Down', exercises=[
|
|
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
|
], rounds=1),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'traditional_strength_training', 'strength',
|
|
)
|
|
size_violations = [
|
|
v for v in violations
|
|
if v.rule_id == 'superset_size'
|
|
]
|
|
self.assertTrue(
|
|
len(size_violations) > 0,
|
|
"Expected superset size warning for 8-exercise superset in strength",
|
|
)
|
|
|
|
def test_superset_focus_repetition_error(self):
|
|
"""Two curl-family exercises in one superset should produce an error."""
|
|
curl_a = _make_exercise(
|
|
name='Alternating Bicep Curls',
|
|
movement_patterns='upper pull',
|
|
is_compound=False,
|
|
exercise_tier='accessory',
|
|
)
|
|
curl_b = _make_exercise(
|
|
name='Bicep Curls',
|
|
movement_patterns='upper pull',
|
|
is_compound=False,
|
|
exercise_tier='accessory',
|
|
)
|
|
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=curl_a, reps=10, order=1),
|
|
_make_entry(exercise=curl_b, reps=10, order=2),
|
|
],
|
|
rounds=3,
|
|
),
|
|
_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',
|
|
)
|
|
repetition_errors = [
|
|
v for v in violations
|
|
if v.rule_id == 'superset_focus_repetition' and v.severity == 'error'
|
|
]
|
|
self.assertTrue(
|
|
repetition_errors,
|
|
f"Expected superset focus repetition error, got {[v.rule_id for v in violations]}",
|
|
)
|
|
|
|
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')
|
|
pull_b = _make_exercise(name='Hammer Curl', movement_patterns='upper pull')
|
|
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=pull_a, reps=10, order=1)],
|
|
rounds=3,
|
|
),
|
|
_make_superset(
|
|
name='Working Set 2',
|
|
exercises=[_make_entry(exercise=pull_b, reps=10, order=1)],
|
|
rounds=3,
|
|
),
|
|
_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',
|
|
)
|
|
adjacent_infos = [
|
|
v for v in violations
|
|
if v.rule_id == 'adjacent_superset_focus_repetition' and v.severity == 'info'
|
|
]
|
|
self.assertTrue(
|
|
adjacent_infos,
|
|
"Expected adjacent superset focus repetition advisory info.",
|
|
)
|
|
|
|
def test_compound_before_isolation_info(self):
|
|
"""Isolation before compound should produce info violation."""
|
|
isolation = _make_exercise(
|
|
is_compound=False, exercise_tier='accessory',
|
|
is_weight=True, is_reps=True,
|
|
)
|
|
compound = _make_exercise(
|
|
is_compound=True, exercise_tier='primary',
|
|
is_weight=True, is_reps=True,
|
|
)
|
|
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=isolation, reps=12, order=1),
|
|
],
|
|
rounds=3,
|
|
),
|
|
_make_superset(
|
|
name='Working Set 2',
|
|
exercises=[
|
|
_make_entry(exercise=compound, reps=5, order=1),
|
|
],
|
|
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, 'hypertrophy', 'hypertrophy',
|
|
)
|
|
order_violations = [
|
|
v for v in violations
|
|
if v.rule_id == 'compound_before_isolation'
|
|
]
|
|
self.assertTrue(
|
|
len(order_violations) > 0,
|
|
"Expected compound_before_isolation info for isolation-first order",
|
|
)
|
|
|
|
def test_unknown_workout_type_does_not_crash(self):
|
|
"""An unknown workout type should not crash validation."""
|
|
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(reps=10)],
|
|
rounds=3,
|
|
),
|
|
_make_superset(name='Cool Down', exercises=[
|
|
_make_entry(exercise=_make_exercise(is_reps=False), duration=30),
|
|
], rounds=1),
|
|
],
|
|
}
|
|
violations = validate_workout(
|
|
workout_spec, 'unknown_type', 'general_fitness',
|
|
)
|
|
# Should not raise; may produce some violations but no crash
|
|
self.assertIsInstance(violations, list)
|
|
|
|
|
|
class TestRuleViolationDataclass(TestCase):
|
|
"""Test the RuleViolation dataclass."""
|
|
|
|
def test_basic_creation(self):
|
|
v = RuleViolation(
|
|
rule_id='test_rule',
|
|
severity='error',
|
|
message='Test message',
|
|
)
|
|
self.assertEqual(v.rule_id, 'test_rule')
|
|
self.assertEqual(v.severity, 'error')
|
|
self.assertEqual(v.message, 'Test message')
|
|
self.assertIsNone(v.actual_value)
|
|
self.assertIsNone(v.expected_range)
|
|
|
|
def test_with_values(self):
|
|
v = RuleViolation(
|
|
rule_id='rep_range_primary',
|
|
severity='error',
|
|
message='Reps out of range',
|
|
actual_value=20,
|
|
expected_range=(3, 6),
|
|
)
|
|
self.assertEqual(v.actual_value, 20)
|
|
self.assertEqual(v.expected_range, (3, 6))
|
|
|
|
|
|
class TestUniversalRules(TestCase):
|
|
"""Verify universal rules have expected values."""
|
|
|
|
def test_push_pull_ratio_min(self):
|
|
self.assertEqual(UNIVERSAL_RULES['push_pull_ratio_min'], 1.0)
|
|
|
|
def test_compound_before_isolation(self):
|
|
self.assertTrue(UNIVERSAL_RULES['compound_before_isolation'])
|
|
|
|
def test_warmup_mandatory(self):
|
|
self.assertTrue(UNIVERSAL_RULES['warmup_mandatory'])
|
|
|
|
def test_max_hiit_duration(self):
|
|
self.assertEqual(UNIVERSAL_RULES['max_hiit_duration_min'], 30)
|
|
|
|
def test_cooldown_stretch_only(self):
|
|
self.assertTrue(UNIVERSAL_RULES['cooldown_stretch_only'])
|