""" Tests for the calibrate_structure_rules management command. Verifies the full 120-rule matrix (8 types x 5 goals x 3 sections) is correctly populated, all values are sane, and the command is idempotent (running it twice doesn't create duplicates). """ from django.test import TestCase from django.core.management import call_command from generator.models import WorkoutStructureRule, WorkoutType WORKOUT_TYPE_NAMES = [ 'traditional_strength_training', 'hypertrophy', 'high_intensity_interval_training', 'functional_strength_training', 'cross_training', 'core_training', 'flexibility', 'cardio', ] GOAL_TYPES = [ 'strength', 'hypertrophy', 'endurance', 'weight_loss', 'general_fitness', ] SECTION_TYPES = ['warm_up', 'working', 'cool_down'] class TestStructureRules(TestCase): """Verify calibrate_structure_rules produces the correct 120-rule matrix.""" @classmethod def setUpTestData(cls): # Create all 8 workout types so the command can find them. cls.workout_types = [] for name in WORKOUT_TYPE_NAMES: wt, _ = WorkoutType.objects.get_or_create(name=name) cls.workout_types.append(wt) # Run the calibration command. call_command('calibrate_structure_rules') # ------------------------------------------------------------------ # Coverage tests # ------------------------------------------------------------------ def test_all_120_combinations_exist(self): """8 types x 5 goals x 3 sections = 120 rules.""" count = WorkoutStructureRule.objects.count() self.assertEqual(count, 120, f'Expected 120 rules, got {count}') def test_each_type_has_15_rules(self): """Each workout type should have 5 goals x 3 sections = 15 rules.""" for wt in self.workout_types: count = WorkoutStructureRule.objects.filter( workout_type=wt, ).count() self.assertEqual( count, 15, f'{wt.name} has {count} rules, expected 15', ) def test_each_type_has_all_sections(self): """Every type must cover warm_up, working, and cool_down.""" for wt in self.workout_types: sections = set( WorkoutStructureRule.objects.filter( workout_type=wt, ).values_list('section_type', flat=True) ) self.assertEqual( sections, {'warm_up', 'working', 'cool_down'}, f'{wt.name} missing sections: ' f'{{"warm_up", "working", "cool_down"}} - {sections}', ) def test_each_type_has_all_goals(self): """Every type must have all 5 goal types.""" for wt in self.workout_types: goals = set( WorkoutStructureRule.objects.filter( workout_type=wt, ).values_list('goal_type', flat=True) ) expected = set(GOAL_TYPES) self.assertEqual( goals, expected, f'{wt.name} goals mismatch: expected {expected}, got {goals}', ) # ------------------------------------------------------------------ # Value sanity tests # ------------------------------------------------------------------ def test_working_rules_have_movement_patterns(self): """All working-section rules must have at least one pattern.""" working_rules = WorkoutStructureRule.objects.filter( section_type='working', ) for rule in working_rules: self.assertTrue( len(rule.movement_patterns) > 0, f'Working rule {rule} has empty movement_patterns', ) def test_warmup_and_cooldown_have_patterns(self): """Warm-up and cool-down rules should also have patterns.""" for section in ('warm_up', 'cool_down'): rules = WorkoutStructureRule.objects.filter(section_type=section) for rule in rules: self.assertTrue( len(rule.movement_patterns) > 0, f'{section} rule {rule} has empty movement_patterns', ) def test_rep_ranges_valid(self): """rep_min <= rep_max, and working rep_min >= 1.""" for rule in WorkoutStructureRule.objects.all(): self.assertLessEqual( rule.typical_rep_range_min, rule.typical_rep_range_max, f'Rule {rule}: rep_min ({rule.typical_rep_range_min}) ' f'> rep_max ({rule.typical_rep_range_max})', ) if rule.section_type == 'working': self.assertGreaterEqual( rule.typical_rep_range_min, 1, f'Rule {rule}: working rep_min below floor', ) def test_duration_ranges_valid(self): """dur_min <= dur_max for every rule.""" for rule in WorkoutStructureRule.objects.all(): self.assertLessEqual( rule.typical_duration_range_min, rule.typical_duration_range_max, f'Rule {rule}: dur_min ({rule.typical_duration_range_min}) ' f'> dur_max ({rule.typical_duration_range_max})', ) def test_warm_up_rounds_are_one(self): """All warm_up sections must have exactly 1 round.""" warmup_rules = WorkoutStructureRule.objects.filter( section_type='warm_up', ) for rule in warmup_rules: self.assertEqual( rule.typical_rounds, 1, f'Warm-up rule {rule} has rounds={rule.typical_rounds}, ' f'expected 1', ) def test_cool_down_rounds_are_one(self): """All cool_down sections must have exactly 1 round.""" cooldown_rules = WorkoutStructureRule.objects.filter( section_type='cool_down', ) for rule in cooldown_rules: self.assertEqual( rule.typical_rounds, 1, f'Cool-down rule {rule} has rounds={rule.typical_rounds}, ' f'expected 1', ) def test_cardio_rounds_not_absurd(self): """Cardio working rounds should be 2-3, not 23-25 (ML artifact).""" cardio_wt = WorkoutType.objects.get(name='cardio') cardio_working = WorkoutStructureRule.objects.filter( workout_type=cardio_wt, section_type='working', ) for rule in cardio_working: self.assertLessEqual( rule.typical_rounds, 5, f'Cardio working {rule.goal_type} has ' f'rounds={rule.typical_rounds}, expected <= 5', ) self.assertGreaterEqual( rule.typical_rounds, 2, f'Cardio working {rule.goal_type} has ' f'rounds={rule.typical_rounds}, expected >= 2', ) def test_cool_down_has_stretch_or_mobility(self): """Cool-down patterns should focus on stretch/mobility.""" cooldown_rules = WorkoutStructureRule.objects.filter( section_type='cool_down', ) stretch_mobility_patterns = { 'mobility', 'mobility - static', 'yoga', 'lower pull - hip hinge', 'cardio/locomotion', } for rule in cooldown_rules: patterns = set(rule.movement_patterns) overlap = patterns & stretch_mobility_patterns self.assertTrue( len(overlap) > 0, f'Cool-down rule {rule} has no stretch/mobility patterns: ' f'{rule.movement_patterns}', ) def test_no_rep_min_below_global_floor(self): """After calibration, no rule should have rep_min < 6 (the floor).""" below_floor = WorkoutStructureRule.objects.filter( typical_rep_range_min__lt=6, typical_rep_range_min__gt=0, ) self.assertEqual( below_floor.count(), 0, f'{below_floor.count()} rules have rep_min below 6', ) # ------------------------------------------------------------------ # Idempotency test # ------------------------------------------------------------------ def test_calibrate_is_idempotent(self): """Running the command again must not create duplicates.""" # Run calibration a second time. call_command('calibrate_structure_rules') count = WorkoutStructureRule.objects.count() self.assertEqual( count, 120, f'After re-run, expected 120 rules, got {count}', ) def test_calibrate_updates_existing_values(self): """If a rule value is changed in DB, re-running restores it.""" # Pick a rule and mutate it. rule = WorkoutStructureRule.objects.filter( section_type='working', goal_type='strength', ).first() original_rounds = rule.typical_rounds rule.typical_rounds = 99 rule.save() # Re-run calibration. call_command('calibrate_structure_rules') rule.refresh_from_db() self.assertEqual( rule.typical_rounds, original_rounds, f'Expected rounds to be restored to {original_rounds}, ' f'got {rule.typical_rounds}', )