Add auto-progression engine (ProgressionService) with rep increase, weight increase, deload, and felt-easy acceleration rules. Add AnalyticsService for user stats, exercise progress charts, and volume-by-day data. New dashboard and schedule routes with Chart.js visualizations. Progression badges shown inline on workout day view. Navigation updated with Dashboard and Schedule links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
"""Tests for the ProgressionService class."""
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from sqlmodel import SQLModel, Session, create_engine
|
|
|
|
from app.models.user import User
|
|
from app.models.exercise import Exercise
|
|
from app.models.workout_day import WorkoutDay
|
|
from app.models.workout_session import WorkoutSession
|
|
from app.models.workout_log import WorkoutLog
|
|
from app.models.user_exercise_program import UserExerciseProgram
|
|
from app.models.progress_log import ProgressLog
|
|
from app.services.progression_service import ProgressionService
|
|
|
|
|
|
class TestProgressionService:
|
|
"""Tests for the auto-progression engine."""
|
|
|
|
def _setup(self):
|
|
"""Create an in-memory DB with a user, exercise, and program."""
|
|
engine = create_engine("sqlite:///:memory:")
|
|
SQLModel.metadata.create_all(engine)
|
|
session = Session(engine)
|
|
|
|
user = User(username="phil", password_hash="h", display_name="Phillip")
|
|
day = WorkoutDay(name="Push", day_number=1, description="Push day")
|
|
exercise = Exercise(
|
|
name="DB Chest Press", muscle_group="Chest",
|
|
workout_day="Push", sets=3, tempo="3-1-2", form_cues="...",
|
|
)
|
|
session.add_all([user, day, exercise])
|
|
session.commit()
|
|
session.refresh(user)
|
|
session.refresh(day)
|
|
session.refresh(exercise)
|
|
|
|
program = UserExerciseProgram(
|
|
user_id=user.id, exercise_id=exercise.id,
|
|
wk1_reps="8", wk4_reps="12",
|
|
wk1_weight="30 lbs", wk4_weight="40 lbs",
|
|
)
|
|
session.add(program)
|
|
session.commit()
|
|
session.refresh(program)
|
|
|
|
service = ProgressionService(session)
|
|
return session, service, user, day, exercise, program
|
|
|
|
def test_suggest_reps_increase(self) -> None:
|
|
"""Should suggest +1-2 reps when below wk4 target."""
|
|
session, service, user, day, exercise, program = self._setup()
|
|
|
|
# Log a session where user did 8 reps (wk1 target)
|
|
ws = WorkoutSession(
|
|
user_id=user.id, workout_day_id=day.id,
|
|
date=date.today() - timedelta(days=7),
|
|
)
|
|
session.add(ws)
|
|
session.commit()
|
|
session.refresh(ws)
|
|
|
|
for set_num in range(1, 4):
|
|
session.add(WorkoutLog(
|
|
session_id=ws.id, exercise_id=exercise.id,
|
|
set_number=set_num, reps_completed=8,
|
|
weight_used="30 lbs", felt_easy=False,
|
|
))
|
|
session.commit()
|
|
|
|
suggestion = service.get_suggestion(user.id, exercise.id)
|
|
assert suggestion is not None
|
|
assert suggestion["suggested_reps"] >= 9 # +1-2 reps
|
|
assert suggestion["suggested_weight"] == "30 lbs" # same weight
|
|
assert suggestion["progression_type"] == "reps_increase"
|
|
session.close()
|
|
|
|
def test_suggest_weight_increase(self) -> None:
|
|
"""Should suggest +5 lbs when at wk4 rep target and felt easy."""
|
|
session, service, user, day, exercise, program = self._setup()
|
|
|
|
# Log two sessions at max reps, all felt easy
|
|
for week_offset in [14, 7]:
|
|
ws = WorkoutSession(
|
|
user_id=user.id, workout_day_id=day.id,
|
|
date=date.today() - timedelta(days=week_offset),
|
|
)
|
|
session.add(ws)
|
|
session.commit()
|
|
session.refresh(ws)
|
|
|
|
for set_num in range(1, 4):
|
|
session.add(WorkoutLog(
|
|
session_id=ws.id, exercise_id=exercise.id,
|
|
set_number=set_num, reps_completed=12,
|
|
weight_used="30 lbs", felt_easy=True,
|
|
))
|
|
session.commit()
|
|
|
|
suggestion = service.get_suggestion(user.id, exercise.id)
|
|
assert suggestion is not None
|
|
assert suggestion["suggested_weight"] == "35 lbs" # +5 lbs
|
|
assert suggestion["progression_type"] == "weight_increase"
|
|
session.close()
|
|
|
|
def test_suggest_deload(self) -> None:
|
|
"""Should suggest deload after 4 weeks of progression."""
|
|
session, service, user, day, exercise, program = self._setup()
|
|
|
|
# Log 4 weeks of sessions (simulate week 5 trigger)
|
|
for week in range(4):
|
|
ws = WorkoutSession(
|
|
user_id=user.id, workout_day_id=day.id,
|
|
date=date.today() - timedelta(days=(4 - week) * 7),
|
|
)
|
|
session.add(ws)
|
|
session.commit()
|
|
session.refresh(ws)
|
|
|
|
for set_num in range(1, 4):
|
|
session.add(WorkoutLog(
|
|
session_id=ws.id, exercise_id=exercise.id,
|
|
set_number=set_num, reps_completed=12,
|
|
weight_used="40 lbs", felt_easy=False,
|
|
))
|
|
session.commit()
|
|
|
|
suggestion = service.get_suggestion(user.id, exercise.id)
|
|
assert suggestion is not None
|
|
# After 4 consecutive weeks, week 5 should be deload
|
|
if suggestion["progression_type"] == "deload":
|
|
assert "32" in suggestion["suggested_weight"] # -20% of 40
|
|
session.close()
|
|
|
|
def test_no_suggestion_without_logs(self) -> None:
|
|
"""Should return program defaults when no logs exist."""
|
|
session, service, user, day, exercise, program = self._setup()
|
|
suggestion = service.get_suggestion(user.id, exercise.id)
|
|
assert suggestion is not None
|
|
assert suggestion["suggested_reps"] == 8 # wk1 default
|
|
assert suggestion["suggested_weight"] == "30 lbs" # wk1 default
|
|
assert suggestion["progression_type"] == "baseline"
|
|
session.close()
|
|
|
|
def test_record_progression(self) -> None:
|
|
"""record_progression should write to progress_log table."""
|
|
session, service, user, day, exercise, program = self._setup()
|
|
service.record_progression(
|
|
user_id=user.id,
|
|
exercise_id=exercise.id,
|
|
suggested_reps=10,
|
|
suggested_weight="30 lbs",
|
|
actual_reps=10,
|
|
actual_weight="30 lbs",
|
|
progression_type="reps_increase",
|
|
)
|
|
from sqlmodel import select
|
|
logs = session.exec(select(ProgressLog)).all()
|
|
assert len(logs) == 1
|
|
assert logs[0].progression_applied == "reps_increase"
|
|
session.close()
|