Files
WerkoutAPI/generator/tests/test_injury_safety.py
Trey t 1c61b80731 workout generator audit: rules engine, structure rules, split patterns, injury UX, metadata cleanup
- Add rules_engine.py with quantitative rules for all 8 workout types
- Add quality gate retry loop in generate_single_workout()
- Expand calibrate_structure_rules to all 120 combinations (8 types × 5 goals × 3 sections)
- Wire WeeklySplitPattern DB records into _pick_weekly_split()
- Enforce movement patterns from WorkoutStructureRule in exercise selection
- Add straight-set strength support (single main lift, 4-6 rounds)
- Add modality consistency check for duration-dominant workout types
- Add InjuryStep component to onboarding and preferences
- Add sibling exercise exclusion in regenerate and preview_day endpoints
- Display generator warnings on dashboard
- Expand fix_rep_durations, fix_exercise_flags, fix_movement_pattern_typo
- Add audit_exercise_data and check_rules_drift management commands
- Add Next.js frontend with dashboard, onboarding, preferences, history pages
- Add generator app with ML-powered workout generation pipeline
- 96 new tests across 7 test modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:07:40 -06:00

165 lines
6.1 KiB
Python

from datetime import date
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework.authtoken.models import Token
from registered_user.models import RegisteredUser
from generator.models import UserPreference, WorkoutType
class TestInjurySafety(TestCase):
"""Tests for injury-related preference round-trip and warning generation."""
def setUp(self):
self.django_user = User.objects.create_user(
username='testuser',
password='testpass123',
email='test@example.com',
)
self.registered_user = RegisteredUser.objects.create(
user=self.django_user,
first_name='Test',
last_name='User',
)
self.token = Token.objects.create(user=self.django_user)
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
self.preference = UserPreference.objects.create(
registered_user=self.registered_user,
days_per_week=3,
)
# Create a basic workout type for generation
self.workout_type = WorkoutType.objects.create(
name='functional_strength_training',
display_name='Functional Strength',
typical_rest_between_sets=60,
typical_intensity='medium',
rep_range_min=8,
rep_range_max=12,
round_range_min=3,
round_range_max=4,
duration_bias=0.3,
superset_size_min=2,
superset_size_max=4,
)
def test_injury_types_roundtrip(self):
"""PUT injury_types, GET back, verify data persists."""
injuries = [
{'type': 'knee', 'severity': 'moderate'},
{'type': 'shoulder', 'severity': 'mild'},
]
response = self.client.put(
'/generator/preferences/update/',
{'injury_types': injuries},
format='json',
)
self.assertEqual(response.status_code, 200)
# GET back
response = self.client.get('/generator/preferences/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data['injury_types']), 2)
types_set = {i['type'] for i in data['injury_types']}
self.assertIn('knee', types_set)
self.assertIn('shoulder', types_set)
def test_injury_types_validation_rejects_invalid_type(self):
"""Invalid injury type should be rejected."""
response = self.client.put(
'/generator/preferences/update/',
{'injury_types': [{'type': 'elbow', 'severity': 'mild'}]},
format='json',
)
self.assertEqual(response.status_code, 400)
def test_injury_types_validation_rejects_invalid_severity(self):
"""Invalid severity should be rejected."""
response = self.client.put(
'/generator/preferences/update/',
{'injury_types': [{'type': 'knee', 'severity': 'extreme'}]},
format='json',
)
self.assertEqual(response.status_code, 400)
def test_severe_knee_excludes_high_impact(self):
"""Set knee:severe, verify the exercise selector filters correctly."""
from generator.services.exercise_selector import ExerciseSelector
self.preference.injury_types = [
{'type': 'knee', 'severity': 'severe'},
]
self.preference.save()
selector = ExerciseSelector(self.preference)
qs = selector._get_filtered_queryset()
# No high-impact exercises should remain
high_impact = qs.filter(impact_level='high')
self.assertEqual(high_impact.count(), 0)
# No medium-impact exercises either (severe lower body)
medium_impact = qs.filter(impact_level='medium')
self.assertEqual(medium_impact.count(), 0)
# Warnings should mention the injury
self.assertTrue(
any('knee' in w.lower() for w in selector.warnings),
f'Expected knee-related warning, got: {selector.warnings}'
)
def test_no_injuries_full_pool(self):
"""Empty injury_types should not exclude any exercises."""
from generator.services.exercise_selector import ExerciseSelector
self.preference.injury_types = []
self.preference.save()
selector = ExerciseSelector(self.preference)
qs = selector._get_filtered_queryset()
# With no injuries, there should be no injury-based warnings
injury_warnings = [w for w in selector.warnings if 'injury' in w.lower()]
self.assertEqual(len(injury_warnings), 0)
def test_warnings_in_preview_response(self):
"""With injuries set, verify warnings key appears in preview response."""
self.preference.injury_types = [
{'type': 'knee', 'severity': 'moderate'},
]
self.preference.save()
self.preference.preferred_workout_types.add(self.workout_type)
response = self.client.post(
'/generator/preview/',
{'week_start_date': '2026-03-02'},
format='json',
)
# Should succeed (200) even if exercise pool is limited
self.assertIn(response.status_code, [200, 500])
if response.status_code == 200:
data = response.json()
# The warnings key should exist if injuries triggered any warnings
if 'warnings' in data:
self.assertIsInstance(data['warnings'], list)
def test_backward_compat_string_injuries(self):
"""Legacy string format should be accepted and normalized."""
response = self.client.put(
'/generator/preferences/update/',
{'injury_types': ['knee', 'shoulder']},
format='json',
)
self.assertEqual(response.status_code, 200)
# Verify normalized to dict format
response = self.client.get('/generator/preferences/')
data = response.json()
for injury in data['injury_types']:
self.assertIn('type', injury)
self.assertIn('severity', injury)
self.assertEqual(injury['severity'], 'moderate')