small-hackathon-trainer / tests /test_models.py
Lucas
Build training coach MVP
f1b0176
Raw
History Blame Contribute Delete
15.1 kB
import unittest
from pydantic import ValidationError
from datetime import date
from training_coach.models import (
CheckIn,
CompletedSession,
CompletedSet,
ContextSignal,
Exercise,
FollowUpQuestion,
LoggedExercise,
LoggedSet,
Muscle,
PainIssue,
ParsedCheckIn,
PlannedExercise,
PrescribedSet,
SessionLog,
SessionPlan,
)
class CheckInTest(unittest.TestCase):
def test_check_in_accepts_mvp_fields(self):
check_in = CheckIn(
raw_text="45 min, slept badly, low energy, back is tight",
time_available_minutes=45,
energy_level="low",
sleep_quality="poor",
sleep_hours=5.5,
soreness="tight back",
pain_or_injury="unsure",
mood_stress="stressed",
notes="Parser should flag the back note for review.",
)
self.assertEqual(check_in.time_available_minutes, 45)
self.assertEqual(check_in.energy_level, "low")
self.assertEqual(check_in.model_dump()["sleep_hours"], 5.5)
def test_check_in_rejects_unknown_energy_level(self):
with self.assertRaises(ValidationError):
CheckIn(energy_level="destroyed")
def test_check_in_rejects_negative_time(self):
with self.assertRaises(ValidationError):
CheckIn(time_available_minutes=-10)
def test_check_in_accepts_multiple_pain_issues(self):
check_in = CheckIn(
raw_text="Right tricep hurts and left hamstring feels tight.",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="moderate",
notes="Right tricep hurts.",
),
PainIssue(
affected_muscle=Muscle.HAMSTRINGS,
severity="mild",
notes="Left hamstring feels tight.",
),
],
)
self.assertEqual(len(check_in.pain_issues), 2)
self.assertEqual(
check_in.model_dump(mode="json")["pain_issues"][0]["affected_muscle"],
"triceps_brachii",
)
def test_pain_issue_accepts_unclear_muscle(self):
issue = PainIssue(
affected_muscle=None,
severity="unsure",
notes="Arm feels weird, exact location unclear.",
)
self.assertIsNone(issue.affected_muscle)
def test_pain_issue_rejects_unknown_severity(self):
with self.assertRaises(ValidationError):
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="annoying",
notes="Right tricep hurts.",
)
def test_pain_issue_requires_notes(self):
with self.assertRaises(ValidationError):
PainIssue(
affected_muscle=Muscle.TRICEPS_BRACHII,
severity="mild",
notes="",
)
class ParsedCheckInTest(unittest.TestCase):
def test_parsed_check_in_tracks_missing_details_and_follow_ups(self):
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="45 min, terrible night, low energy",
time_available_minutes=45,
energy_level="low",
sleep_quality="poor",
),
missing_fields=["sleep_hours"],
follow_up_items=[
FollowUpQuestion(
field="sleep_hours",
question="How many hours did you sleep?",
reason="Sleep quality is poor but duration is missing.",
)
],
follow_up_questions=["How many hours did you sleep?"],
)
self.assertEqual(parsed.check_in.sleep_quality, "poor")
self.assertIn("sleep_hours", parsed.missing_fields)
self.assertEqual(parsed.follow_up_items[0].field, "sleep_hours")
def test_parsed_check_in_tracks_adjacent_context_signals(self):
parsed = ParsedCheckIn(
check_in=CheckIn(raw_text="Yesterday I ran a 10k."),
context_signals=[
ContextSignal(
label="recent_endurance_run",
evidence="Yesterday I ran a 10k.",
follow_up_question=(
"How are your legs and energy after yesterday's run?"
),
)
],
follow_up_questions=[
"How are your legs and energy after yesterday's run?"
],
)
self.assertEqual(parsed.context_signals[0].label, "recent_endurance_run")
self.assertIn("10k", parsed.context_signals[0].evidence)
def test_context_signal_rejects_unknown_label(self):
with self.assertRaises(ValidationError):
ContextSignal(
label="Recent Activity",
evidence="Yesterday I ran a 10k.",
)
def test_parsed_check_in_rejects_extra_fields(self):
with self.assertRaises(ValidationError):
ParsedCheckIn(
check_in=CheckIn(raw_text="60 min"),
invented_training_action="skip legs",
)
class ExerciseTest(unittest.TestCase):
def test_exercise_accepts_primary_and_secondary_muscle_groups(self):
exercise = Exercise(
id="decline-bench-press",
name="Decline bench press",
primary_muscle_group=Muscle.PECTORALIS_MAJOR,
secondary_muscle_groups=[
Muscle.TRICEPS_BRACHII,
Muscle.FRONT_DELTOID,
],
is_compound=True,
rep_range_low=6,
rep_range_high=10,
)
self.assertEqual(exercise.primary_muscle_group, Muscle.PECTORALIS_MAJOR)
self.assertIn(Muscle.TRICEPS_BRACHII, exercise.secondary_muscle_groups)
self.assertEqual(
exercise.model_dump(mode="json")["secondary_muscle_groups"],
["triceps_brachii", "front_deltoid"],
)
def test_exercise_rejects_empty_id(self):
with self.assertRaises(ValidationError):
Exercise(
id="",
name="Decline bench press",
primary_muscle_group=Muscle.PECTORALIS_MAJOR,
is_compound=True,
rep_range_low=6,
rep_range_high=10,
)
def test_exercise_rejects_invalid_rep_range_numbers(self):
with self.assertRaises(ValidationError):
Exercise(
id="decline-bench-press",
name="Decline bench press",
primary_muscle_group=Muscle.PECTORALIS_MAJOR,
is_compound=True,
rep_range_low=0,
rep_range_high=10,
)
def test_exercise_rejects_unknown_muscle(self):
with self.assertRaises(ValidationError):
Exercise(
id="decline-bench-press",
name="Decline bench press",
primary_muscle_group="chest",
is_compound=True,
rep_range_low=6,
rep_range_high=10,
)
def test_exercise_rejects_inverted_rep_range(self):
with self.assertRaises(ValidationError):
Exercise(
id="decline-bench-press",
name="Decline bench press",
primary_muscle_group=Muscle.PECTORALIS_MAJOR,
is_compound=True,
rep_range_low=12,
rep_range_high=8,
)
class SessionPlanTest(unittest.TestCase):
def test_session_plan_accepts_ordered_planned_exercises(self):
check_in = CheckIn(
raw_text="60 min, decent sleep, medium energy",
time_available_minutes=60,
energy_level="medium",
sleep_quality="okay",
pain_or_injury="no",
mood_stress="neutral",
)
first_set = PrescribedSet(
set_number=1,
target_reps_low=8,
target_reps_high=10,
target_load=80,
target_rir=2,
)
planned_exercise = PlannedExercise(
exercise_id="decline-bench-press",
order=1,
prescribed_sets=[first_set],
rest_seconds=90,
)
plan = SessionPlan(
date=date(2026, 6, 10),
check_in=check_in,
planned_exercises=[planned_exercise],
notes="Hardcoded test plan.",
)
self.assertEqual(plan.date.isoformat(), "2026-06-10")
self.assertEqual(plan.planned_exercises[0].exercise_id, "decline-bench-press")
self.assertEqual(plan.planned_exercises[0].rest_seconds, 90)
self.assertEqual(
plan.model_dump(mode="json")["planned_exercises"][0]["prescribed_sets"][0][
"target_load"
],
80.0,
)
def test_prescribed_set_rejects_inverted_target_rep_range(self):
with self.assertRaises(ValidationError):
PrescribedSet(
set_number=1,
target_reps_low=12,
target_reps_high=8,
)
def test_prescribed_set_rejects_target_reps_outside_range(self):
with self.assertRaises(ValidationError):
PrescribedSet(
set_number=1,
target_reps_low=8,
target_reps_high=12,
target_reps=14,
)
def test_prescribed_set_rejects_extra_fields(self):
with self.assertRaises(ValidationError):
PrescribedSet(
exercise_id="decline-bench-press",
set_number=1,
target_reps_low=8,
target_reps_high=10,
)
def test_planned_exercise_requires_at_least_one_prescribed_set(self):
with self.assertRaises(ValidationError):
PlannedExercise(
exercise_id="decline-bench-press",
order=1,
prescribed_sets=[],
)
def test_session_plan_requires_at_least_one_planned_exercise(self):
with self.assertRaises(ValidationError):
SessionPlan(
date=date(2026, 6, 10),
check_in=CheckIn(raw_text="60 min"),
planned_exercises=[],
)
class SessionLogTest(unittest.TestCase):
def test_session_log_accepts_ordered_logged_exercises(self):
planned_session = SessionPlan(
date=date(2026, 6, 10),
check_in=CheckIn(raw_text="60 min", time_available_minutes=60),
planned_exercises=[
PlannedExercise(
exercise_id="bench-press",
order=1,
prescribed_sets=[
PrescribedSet(
set_number=1,
target_reps_low=8,
target_reps_high=10,
target_load=80,
target_rir=2,
)
],
)
],
)
logged_set = LoggedSet(
set_number=1,
actual_reps=9,
actual_load=80,
rpe=8.5,
rest_seconds_before=180,
notes="Longer rest than usual.",
)
session_log = SessionLog(
date=date(2026, 6, 10),
planned_session=planned_session,
logged_exercises=[
LoggedExercise(
exercise_id="bench-press",
order=1,
logged_sets=[logged_set],
)
],
notes="Classic straight sets only.",
)
self.assertEqual(session_log.logged_exercises[0].logged_sets[0].rpe, 8.5)
self.assertEqual(
session_log.model_dump(mode="json")["logged_exercises"][0]["logged_sets"][0][
"rest_seconds_before"
],
180,
)
def test_logged_set_rejects_invalid_rpe(self):
with self.assertRaises(ValidationError):
LoggedSet(
set_number=1,
actual_reps=9,
actual_load=80,
rpe=12,
)
def test_logged_exercise_requires_at_least_one_logged_set(self):
with self.assertRaises(ValidationError):
LoggedExercise(
exercise_id="bench-press",
order=1,
logged_sets=[],
)
def test_session_log_requires_at_least_one_logged_exercise(self):
planned_session = SessionPlan(
date=date(2026, 6, 10),
check_in=CheckIn(raw_text="60 min"),
planned_exercises=[
PlannedExercise(
exercise_id="bench-press",
order=1,
prescribed_sets=[
PrescribedSet(
set_number=1,
target_reps_low=8,
target_reps_high=10,
)
],
)
],
)
with self.assertRaises(ValidationError):
SessionLog(
date=date(2026, 6, 10),
planned_session=planned_session,
logged_exercises=[],
)
class CompletedSessionTest(unittest.TestCase):
def test_completed_session_accepts_minimal_mvp_history(self):
completed_set = CompletedSet(
exercise_id="dumbbell-row",
set_number=1,
actual_reps=16,
actual_load=32.5,
rpe=8,
)
completed_session = CompletedSession(
date=date(2026, 6, 11),
day_number=1,
completed_sets=[completed_set],
notes="Minimal MVP log.",
)
self.assertEqual(completed_session.day_number, 1)
self.assertEqual(completed_session.completed_sets[0].actual_load, 32.5)
self.assertEqual(
completed_session.model_dump(mode="json")["completed_sets"][0]["exercise_id"],
"dumbbell-row",
)
def test_completed_session_rejects_unknown_day_number(self):
with self.assertRaises(ValidationError):
CompletedSession(
date=date(2026, 6, 11),
day_number=5,
completed_sets=[
CompletedSet(
exercise_id="dumbbell-row",
set_number=1,
actual_reps=16,
actual_load=32.5,
)
],
)
def test_completed_session_requires_at_least_one_completed_set(self):
with self.assertRaises(ValidationError):
CompletedSession(
date=date(2026, 6, 11),
day_number=1,
completed_sets=[],
)
if __name__ == "__main__":
unittest.main()