Lucas
Fix stale history header on Space refresh
6cab622
Raw
History Blame Contribute Delete
13.7 kB
import unittest
from unittest.mock import patch
from app import (
LOG_HEADERS,
build_preview,
parse_check_in,
persist_logged_sets,
render_current_hero,
reset_check_in_conversation,
save_completed_session,
send_check_in_message,
)
from training_coach.models import (
CheckIn,
CompletedSession,
CompletedSet,
ContextSignal,
PainIssue,
ParsedCheckIn,
)
class AppTest(unittest.TestCase):
def test_parse_check_in_requires_text(self):
result = parse_check_in("")
self.assertEqual(result[-1], "Write a check-in first.")
def test_parse_check_in_populates_fields_and_summary(self):
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="45 min, terrible sleep, right tricep hurts",
time_available_minutes=45,
energy_level="low",
sleep_quality="poor",
sleep_hours=None,
soreness="right tricep hurts",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle="triceps_brachii",
severity="moderate",
notes="Right tricep hurts.",
)
],
mood_stress="stressed",
),
follow_up_questions=["How many hours did you sleep?"],
context_signals=[
ContextSignal(
label="recent_endurance_run",
evidence="Yesterday I ran.",
follow_up_question="How are your legs?",
)
],
)
with patch("app.parse_check_in_with_configured_backend", return_value=parsed):
result = parse_check_in("45 min, terrible sleep, right tricep hurts")
self.assertEqual(result[0], 45)
self.assertEqual(result[1], "low")
self.assertEqual(result[2], "poor")
self.assertEqual(result[5], "yes")
self.assertEqual(result[6], "stressed")
self.assertEqual(result[7][0]["affected_muscle"], "triceps_brachii")
self.assertNotIn("How many hours", result[8])
self.assertIn("recent_endurance_run", result[8])
self.assertIn("triceps_brachii", result[8])
def test_parse_check_in_reports_parser_failure(self):
with patch("app.parse_check_in_with_configured_backend", side_effect=RuntimeError("no ollama")):
result = parse_check_in("45 min")
self.assertIn("Parser failed: no ollama", result[-1])
def test_send_check_in_message_appends_user_text_and_updates_chat(self):
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="45 min today\nI slept 5 hours",
time_available_minutes=45,
sleep_quality="okay",
sleep_hours=5,
pain_or_injury="no",
)
)
with patch("app.parse_check_in_with_configured_backend", return_value=parsed) as parse:
result = send_check_in_message(
"I slept 5 hours",
[{"role": "user", "content": "45 min today"}],
"45 min today",
)
self.assertEqual(result[0], "")
self.assertEqual(result[2], "45 min today\nI slept 5 hours")
self.assertEqual(result[3], 45)
self.assertEqual(result[6], 5)
self.assertEqual(result[1][-2]["role"], "user")
self.assertEqual(result[1][-1]["role"], "assistant")
parse.assert_called_once_with("45 min today\nI slept 5 hours")
def test_send_check_in_message_puts_follow_up_questions_only_in_chat(self):
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="I feel tired.",
energy_level="low",
pain_or_injury="unsure",
),
follow_up_questions=["How many minutes do you have today?"],
)
with patch("app.parse_check_in_with_configured_backend", return_value=parsed):
result = send_check_in_message("I feel tired.", [], "")
self.assertIn("How many minutes", result[1][-1]["content"])
self.assertNotIn("How many minutes", result[-1])
self.assertEqual(result[-1], "No extra parser notes.")
def test_send_check_in_message_preserves_previous_pain_issues(self):
previous_pain_issues = [
PainIssue(
affected_muscle="biceps_brachii",
severity="mild",
notes="Bicep is hurting.",
).model_dump(mode="json")
]
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="bicep hurts\nand my left leg also\nhamstring, mild but sharp",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle="hamstrings",
severity="mild",
notes="Left hamstring pain, mild but sharp.",
)
],
)
)
with patch("app.parse_check_in_with_configured_backend", return_value=parsed):
result = send_check_in_message(
"hamstring, mild but sharp",
[],
"bicep hurts\nand my left leg also",
previous_pain_issues,
)
pain_issues = result[10]
self.assertEqual(
[issue["affected_muscle"] for issue in pain_issues],
["biceps_brachii", "hamstrings"],
)
self.assertIn("biceps_brachii", result[-1])
self.assertIn("hamstrings", result[-1])
def test_parse_check_in_omits_unsure_pain_severity_from_panel(self):
parsed = ParsedCheckIn(
check_in=CheckIn(
raw_text="yes gluteus_maximus not recovered from last training round",
pain_or_injury="yes",
pain_issues=[
PainIssue(
affected_muscle="gluteus_maximus",
severity="unsure",
notes="gluteus_maximus not recovered from last training round",
)
],
),
)
with patch("app.parse_check_in_with_configured_backend", return_value=parsed):
result = parse_check_in(
"yes gluteus_maximus not recovered from last training round"
)
self.assertIn(
"- gluteus_maximus: gluteus_maximus not recovered",
result[-1],
)
self.assertNotIn("(unsure)", result[-1])
def test_reset_check_in_conversation_clears_chat_and_fields(self):
result = reset_check_in_conversation()
self.assertEqual(result[0], "")
self.assertEqual(result[1], [])
self.assertEqual(result[2], "")
self.assertEqual(result[3], 60)
self.assertEqual(result[-1], "Write a check-in first.")
def test_build_preview_uses_next_day_from_completed_history(self):
completed_set = CompletedSet(
exercise_id="dumbbell-row",
set_number=1,
actual_reps=16,
actual_load=32.5,
)
completed_session = CompletedSession(
date="2026-06-11",
day_number=1,
completed_sets=[completed_set],
)
with patch(
"app.history_store.load_completed_sessions",
return_value=[completed_session],
):
preview, log_rows, day_number = build_preview(
check_in="60 min, medium energy",
time_minutes=60,
energy="medium",
sleep="okay",
sleep_hours=None,
soreness="",
pain_or_injury="no",
mood="neutral",
)
self.assertEqual(day_number, 2)
self.assertIn("Day 2", preview)
self.assertIn("Barbell Skullcrusher", preview)
self.assertNotIn("Hardcoded session preview", preview)
self.assertEqual(log_rows[0][LOG_HEADERS.index("exercise_id")], "barbell-skullcrusher")
self.assertEqual(log_rows[0][LOG_HEADERS.index("set_number")], "1")
self.assertEqual(log_rows[0][LOG_HEADERS.index("target_reps")], "8 (range 6-10)")
self.assertEqual(log_rows[0][LOG_HEADERS.index("actual_reps")], "8")
self.assertEqual(log_rows[0][LOG_HEADERS.index("actual_load")], "")
def test_render_current_hero_uses_next_day_from_current_history(self):
completed_session = CompletedSession(
date="2026-06-11",
day_number=2,
completed_sets=[
CompletedSet(
exercise_id="barbell-skullcrusher",
set_number=1,
actual_reps=8,
actual_load=20,
)
],
)
with patch(
"app.history_store.load_completed_sessions",
return_value=[completed_session],
):
hero_html = render_current_hero()
self.assertIn("Day 3", hero_html)
def test_build_preview_prefills_recommended_load_from_progression(self):
completed_set = CompletedSet(
exercise_id="dumbbell-pullover",
set_number=1,
actual_reps=15,
actual_load=10,
)
completed_sessions = [
CompletedSession(
date="2026-06-11",
day_number=1,
completed_sets=[
completed_set,
completed_set.model_copy(update={"set_number": 2}),
completed_set.model_copy(update={"set_number": 3}),
],
),
CompletedSession(
date="2026-06-12",
day_number=4,
completed_sets=[
CompletedSet(
exercise_id="goblet-squat",
set_number=1,
actual_reps=12,
actual_load=10,
)
],
),
]
with patch(
"app.history_store.load_completed_sessions",
return_value=completed_sessions,
):
preview, log_rows, day_number = build_preview(
check_in="60 min, medium energy",
time_minutes=60,
energy="medium",
sleep="okay",
sleep_hours=None,
soreness="",
pain_or_injury="no",
mood="neutral",
)
self.assertEqual(day_number, 1)
self.assertIn("Target load: 11 kg", preview)
self.assertEqual(log_rows[0][LOG_HEADERS.index("actual_load")], "11")
def test_build_preview_filters_exercises_for_parsed_pain_issue(self):
pain_issues_state = [
{
"affected_muscle": "triceps_brachii",
"severity": "moderate",
"notes": "Right tricep hurts.",
}
]
with patch("app.history_store.load_completed_sessions", return_value=[]):
preview, log_rows, day_number = build_preview(
check_in="Right tricep hurts.",
time_minutes=60,
energy="medium",
sleep="okay",
sleep_hours=None,
soreness="right tricep hurts",
pain_or_injury="yes",
mood="neutral",
pain_issues_state=pain_issues_state,
)
self.assertEqual(day_number, 1)
self.assertNotIn("Barbell Incline Bench Press", preview)
self.assertNotIn("Dumbbell Pullover", preview)
self.assertIn("Pain filter removed", preview)
exercise_ids = [row[LOG_HEADERS.index("exercise_id")] for row in log_rows]
self.assertNotIn("barbell-incline-bench-press", exercise_ids)
def test_save_completed_session_requires_completed_sets(self):
result = save_completed_session(1, [])
self.assertIn("Add at least one completed set", result)
def test_save_completed_session_appends_minimal_history(self):
log_rows = [
["dumbbell-row", "1", "16", "16", "32.5", "8", "Good set."],
["dumbbell-row", "2", "16", "16", "", "", ""],
]
with patch("app.history_store.append_completed_session") as append_completed:
result = save_completed_session(1, log_rows)
self.assertIn("Saved Day 1 with 1 completed sets", result)
saved_session = append_completed.call_args.args[0]
self.assertEqual(saved_session.day_number, 1)
self.assertEqual(len(saved_session.completed_sets), 1)
self.assertEqual(saved_session.completed_sets[0].actual_reps, 16)
self.assertEqual(saved_session.completed_sets[0].actual_load, 32.5)
def test_persist_logged_sets_reports_same_next_day_as_header(self):
planned_rows = [["dumbbell-row", "1", "16", "16", "32.5", "", ""]]
with patch("app.history_store.append_completed_session"), patch(
"app._safe_next_day",
return_value=3,
):
message, hero_html = persist_logged_sets(
2,
planned_rows,
[16],
[32.5],
[8],
["Good set."],
[True],
)
self.assertIn("Saved Day 2", message)
self.assertIn("Next up: Day 3.", message)
self.assertIn("Day 3", hero_html)
if __name__ == "__main__":
unittest.main()