small-hackathon-trainer / tests /test_engine.py
Lucas
Build training coach MVP
f1b0176
Raw
History Blame Contribute Delete
22.6 kB
from datetime import date
import unittest
from training_coach.engine import (
EXERCISE_MUSCLE_MAP,
MVP_LOAD_INCREMENT_KG,
apply_pain_filter,
apply_time_compression,
apply_readiness_modifier,
apply_double_progression,
build_session_for_day,
next_day_after,
readiness_score,
suggest_next_training_day,
)
from training_coach.models import CheckIn, CompletedSession, CompletedSet, Muscle, PainIssue
class EngineTemplateTest(unittest.TestCase):
def test_day_1_preserves_exercise_order_and_prescription(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text="60 min, medium energy"),
)
self.assertEqual(len(plan.planned_exercises), 5)
self.assertEqual(plan.planned_exercises[0].exercise_id, "dumbbell-pullover")
self.assertEqual(plan.planned_exercises[0].order, 1)
self.assertEqual(plan.planned_exercises[0].rest_seconds, 60)
self.assertEqual(len(plan.planned_exercises[0].prescribed_sets), 3)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_reps_low, 11)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_reps_high, 15)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_reps, 13)
self.assertEqual(plan.planned_exercises[-1].exercise_id, "goblet-squat")
self.assertEqual(len(plan.planned_exercises[-1].prescribed_sets), 4)
def test_day_2_preserves_rep_range_and_notes_with_mvp_rest(self):
plan = build_session_for_day(2, date(2026, 6, 11), CheckIn(raw_text=""))
triceps_extension = plan.planned_exercises[2]
self.assertEqual(triceps_extension.exercise_id, "barbell-triceps-extension")
self.assertEqual(triceps_extension.rest_seconds, 60)
self.assertEqual(triceps_extension.prescribed_sets[0].target_reps_low, 10)
self.assertEqual(triceps_extension.prescribed_sets[0].target_reps_high, 12)
self.assertIn("Lean-away", plan.planned_exercises[1].notes)
def test_day_3_preserves_row_reps_with_mvp_rest(self):
plan = build_session_for_day(3, date(2026, 6, 11), CheckIn(raw_text=""))
dumbbell_row = plan.planned_exercises[2]
self.assertEqual(dumbbell_row.exercise_id, "dumbbell-row")
self.assertEqual(dumbbell_row.rest_seconds, 60)
self.assertEqual(dumbbell_row.prescribed_sets[0].target_reps_low, 22)
self.assertEqual(dumbbell_row.prescribed_sets[0].target_reps_high, 26)
self.assertEqual(dumbbell_row.prescribed_sets[0].target_reps, 24)
def test_day_4_uses_mvp_rest_for_goblet_squat(self):
plan = build_session_for_day(4, date(2026, 6, 11), CheckIn(raw_text=""))
goblet_squat = plan.planned_exercises[5]
self.assertEqual(goblet_squat.exercise_id, "goblet-squat")
self.assertEqual(goblet_squat.rest_seconds, 60)
def test_all_template_exercises_use_one_minute_rest_for_mvp(self):
for day_number in range(1, 5):
plan = build_session_for_day(
day_number,
date(2026, 6, 11),
CheckIn(raw_text=""),
)
self.assertTrue(
all(exercise.rest_seconds == 60 for exercise in plan.planned_exercises)
)
def test_rejects_unknown_day(self):
with self.assertRaises(ValueError):
build_session_for_day(5, date(2026, 6, 11), CheckIn(raw_text=""))
def test_next_day_rotates_through_four_day_template(self):
self.assertEqual(next_day_after(1), 2)
self.assertEqual(next_day_after(2), 3)
self.assertEqual(next_day_after(3), 4)
self.assertEqual(next_day_after(4), 1)
def test_next_day_rejects_unknown_day(self):
with self.assertRaises(ValueError):
next_day_after(0)
def test_suggest_next_training_day_starts_at_day_1_without_history(self):
self.assertEqual(suggest_next_training_day([]), 1)
def test_suggest_next_training_day_uses_last_completed_session(self):
completed_set = CompletedSet(
exercise_id="dumbbell-row",
set_number=1,
actual_reps=16,
actual_load=32.5,
)
history = [
CompletedSession(
date=date(2026, 6, 10),
day_number=1,
completed_sets=[completed_set],
),
CompletedSession(
date=date(2026, 6, 11),
day_number=4,
completed_sets=[completed_set],
),
]
self.assertEqual(suggest_next_training_day(history), 1)
class DoubleProgressionTest(unittest.TestCase):
def test_mvp_load_increment_is_one_kg(self):
self.assertEqual(MVP_LOAD_INCREMENT_KG, 1.0)
def test_no_history_leaves_target_load_empty(self):
plan = build_session_for_day(1, date(2026, 6, 11), CheckIn(raw_text=""))
progressed = apply_double_progression(plan.planned_exercises[0], [])
self.assertIsNone(progressed.prescribed_sets[0].target_load)
def test_hitting_top_of_range_on_all_sets_adds_one_kg_and_resets_reps(self):
previous_session = CompletedSession(
date=date(2026, 6, 10),
day_number=1,
completed_sets=[
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=1,
actual_reps=15,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=2,
actual_reps=15,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=3,
actual_reps=15,
actual_load=20,
),
],
)
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text=""),
completed_sessions=[previous_session],
)
target_loads = [
prescribed_set.target_load
for prescribed_set in plan.planned_exercises[0].prescribed_sets
]
self.assertEqual(target_loads, [21, 21, 21])
target_reps = [
prescribed_set.target_reps
for prescribed_set in plan.planned_exercises[0].prescribed_sets
]
self.assertEqual(target_reps, [11, 11, 11])
self.assertIn("add 1 kg", plan.planned_exercises[0].notes)
def test_missing_top_of_range_repeats_latest_load_and_adds_two_reps(self):
previous_session = CompletedSession(
date=date(2026, 6, 10),
day_number=1,
completed_sets=[
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=1,
actual_reps=13,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=2,
actual_reps=12,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=3,
actual_reps=13,
actual_load=20,
),
],
)
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text=""),
completed_sessions=[previous_session],
)
target_loads = [
prescribed_set.target_load
for prescribed_set in plan.planned_exercises[0].prescribed_sets
]
self.assertEqual(target_loads, [20, 20, 20])
target_reps = [
prescribed_set.target_reps
for prescribed_set in plan.planned_exercises[0].prescribed_sets
]
self.assertEqual(target_reps, [15, 14, 15])
self.assertIn("repeat latest load", plan.planned_exercises[0].notes)
def test_progression_uses_latest_matching_exercise_exposure(self):
older_session = CompletedSession(
date=date(2026, 6, 9),
day_number=1,
completed_sets=[
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=1,
actual_reps=13,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=2,
actual_reps=13,
actual_load=20,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=3,
actual_reps=13,
actual_load=20,
),
],
)
latest_session = CompletedSession(
date=date(2026, 6, 10),
day_number=1,
completed_sets=[
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=1,
actual_reps=12,
actual_load=22,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=2,
actual_reps=12,
actual_load=22,
),
CompletedSet(
exercise_id="dumbbell-pullover",
set_number=3,
actual_reps=12,
actual_load=22,
),
],
)
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text=""),
completed_sessions=[older_session, latest_session],
)
target_loads = [
prescribed_set.target_load
for prescribed_set in plan.planned_exercises[0].prescribed_sets
]
self.assertEqual(target_loads, [22, 22, 22])
class PainFilterTest(unittest.TestCase):
def test_exercise_muscle_map_tags_incline_bench_with_triceps(self):
self.assertIn(
Muscle.TRICEPS_BRACHII,
EXERCISE_MUSCLE_MAP["barbell-incline-bench-press"],
)
def test_triceps_pain_removes_pressing_and_triceps_exercises(self):
check_in = CheckIn(
raw_text="My tricep hurts.",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="moderate",
notes="Tricep hurts.",
)
],
)
plan = build_session_for_day(1, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("barbell-incline-bench-press", exercise_ids)
self.assertNotIn("dumbbell-pullover", exercise_ids)
self.assertIn("dumbbell-row", exercise_ids)
self.assertIn("Pain filter removed", plan.notes)
self.assertEqual([exercise.order for exercise in plan.planned_exercises], [1, 2, 3])
def test_biceps_pain_removes_bicep_curl_exercises(self):
check_in = CheckIn(
raw_text="My bicep tore.",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle=Muscle.BICEPS_BRACHII,
severity="severe",
notes="Bicep tore.",
)
],
)
plan = build_session_for_day(4, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("ez-bar-biceps-curl", exercise_ids)
self.assertNotIn("cross-body-hammer-curl", exercise_ids)
self.assertIn("dumbbell-lateral-raise", exercise_ids)
self.assertIn("Pain filter removed", plan.notes)
def test_manual_soreness_constraints_bicep_tore_filters_bicep_curls(self):
check_in = CheckIn(
raw_text="",
soreness="bicep tore",
pain_or_injury="unsure",
)
plan = build_session_for_day(4, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("ez-bar-biceps-curl", exercise_ids)
self.assertNotIn("cross-body-hammer-curl", exercise_ids)
self.assertIn("Pain filter removed", plan.notes)
def test_manual_soreness_constraints_calves_pain_filters_calf_raises(self):
check_in = CheckIn(
raw_text="",
soreness="calves are cooked and in pain",
pain_or_injury="unsure",
)
plan = build_session_for_day(4, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("barbell-standing-calf-raise", exercise_ids)
self.assertIn("ez-bar-biceps-curl", exercise_ids)
self.assertIn("Pain filter removed", plan.notes)
def test_manual_pain_yes_with_bicep_text_filters_bicep_curls(self):
check_in = CheckIn(
raw_text="",
soreness="bicep feels bad",
pain_or_injury="yes",
)
plan = build_session_for_day(4, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("ez-bar-biceps-curl", exercise_ids)
self.assertNotIn("cross-body-hammer-curl", exercise_ids)
def test_unclear_or_unsure_pain_does_not_filter_until_clarified(self):
check_in = CheckIn(
raw_text="My arm feels weird.",
pain_or_injury="unsure",
pain_issues=[
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="unsure",
notes="Arm feels weird.",
)
],
)
plan = build_session_for_day(1, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertIn("barbell-incline-bench-press", exercise_ids)
def test_apply_pain_filter_returns_original_plan_when_no_known_pain_muscle(self):
plan = build_session_for_day(1, date(2026, 6, 11), CheckIn(raw_text=""))
filtered, notes = apply_pain_filter(plan.planned_exercises, CheckIn(raw_text=""))
self.assertEqual(len(filtered), len(plan.planned_exercises))
self.assertEqual(notes, [])
class TimeCompressionTest(unittest.TestCase):
def test_sixty_minutes_keeps_full_plan(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text="60 min", time_available_minutes=60),
)
self.assertEqual(len(plan.planned_exercises), 5)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
16,
)
self.assertNotIn("Time compression", plan.notes)
def test_fifty_minutes_cuts_to_about_seventy_five_percent_sets(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text="50 min", time_available_minutes=50),
)
set_counts = [len(exercise.prescribed_sets) for exercise in plan.planned_exercises]
self.assertEqual(sum(set_counts), 12)
self.assertEqual(set_counts, [3, 3, 2, 2, 2])
self.assertIn("Time compression", plan.notes)
def test_thirty_five_minutes_cuts_to_about_sixty_percent_sets(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text="35 min", time_available_minutes=35),
)
set_counts = [len(exercise.prescribed_sets) for exercise in plan.planned_exercises]
self.assertEqual(sum(set_counts), 10)
self.assertEqual(set_counts[0], 3)
self.assertEqual(set_counts[-1], 1)
self.assertIn("35 minutes", plan.notes)
def test_under_thirty_minutes_keeps_first_four_exercises_and_caps_sets(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(raw_text="25 min", time_available_minutes=25),
)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertEqual(
exercise_ids,
[
"dumbbell-pullover",
"dumbbell-row",
"barbell-incline-bench-press",
"dumbbell-incline-chest-fly",
],
)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
8,
)
self.assertIn("under 30 minutes", plan.notes)
def test_time_compression_runs_after_pain_filter(self):
check_in = CheckIn(
raw_text="25 min, tricep hurts",
time_available_minutes=25,
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="moderate",
notes="Tricep hurts.",
)
],
)
plan = build_session_for_day(1, date(2026, 6, 11), check_in)
exercise_ids = [exercise.exercise_id for exercise in plan.planned_exercises]
self.assertNotIn("dumbbell-pullover", exercise_ids)
self.assertNotIn("barbell-incline-bench-press", exercise_ids)
self.assertLessEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
8,
)
self.assertIn("Pain filter removed", plan.notes)
self.assertIn("Time compression", plan.notes)
def test_apply_time_compression_can_be_called_directly(self):
plan = build_session_for_day(1, date(2026, 6, 11), CheckIn(raw_text=""))
compressed, notes = apply_time_compression(
plan.planned_exercises,
CheckIn(raw_text="45 min", time_available_minutes=45),
)
self.assertLess(
sum(len(exercise.prescribed_sets) for exercise in compressed),
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
)
self.assertTrue(notes)
class ReadinessModifierTest(unittest.TestCase):
def test_readiness_score_uses_neutral_defaults(self):
score = readiness_score(CheckIn(raw_text=""))
self.assertEqual(score, 3.0)
def test_normal_readiness_sets_base_target_rir_without_cutting_sets(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(
raw_text="60 min, okay sleep, medium energy",
time_available_minutes=60,
sleep_quality="okay",
energy_level="medium",
mood_stress="neutral",
),
)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
16,
)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_rir, 2)
self.assertIn("Readiness: normal", plan.notes)
def test_low_readiness_reduces_sets_and_adds_one_rir(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(
raw_text="60 min, okay sleep, low energy",
time_available_minutes=60,
sleep_quality="okay",
sleep_hours=6.5,
energy_level="low",
mood_stress="neutral",
),
)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
13,
)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_rir, 3)
self.assertIn("Readiness: low", plan.notes)
def test_very_low_readiness_reduces_sets_and_adds_two_rir(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(
raw_text="60 min, poor sleep, low energy, sore and stressed",
time_available_minutes=60,
sleep_quality="poor",
sleep_hours=4.5,
energy_level="low",
soreness="very sore",
mood_stress="stressed",
),
)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
8,
)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_rir, 4)
self.assertIn("Readiness: very low", plan.notes)
self.assertIn("Sleep override", plan.notes)
def test_high_readiness_does_not_auto_add_work_for_mvp(self):
plan = build_session_for_day(
1,
date(2026, 6, 11),
CheckIn(
raw_text="90 min, good sleep, high energy, ready",
time_available_minutes=90,
sleep_quality="good",
sleep_hours=8,
energy_level="high",
soreness="no soreness",
mood_stress="ready",
),
)
self.assertEqual(
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
16,
)
self.assertEqual(plan.planned_exercises[0].prescribed_sets[0].target_rir, 2)
self.assertIn("Readiness: high", plan.notes)
def test_apply_readiness_modifier_can_be_called_directly(self):
plan = build_session_for_day(1, date(2026, 6, 11), CheckIn(raw_text=""))
modified, notes = apply_readiness_modifier(
plan.planned_exercises,
CheckIn(
raw_text="low energy",
sleep_quality="okay",
sleep_hours=6.5,
energy_level="low",
mood_stress="neutral",
),
)
self.assertLess(
sum(len(exercise.prescribed_sets) for exercise in modified),
sum(len(exercise.prescribed_sets) for exercise in plan.planned_exercises),
)
self.assertTrue(notes)
if __name__ == "__main__":
unittest.main()