"""Tests for the text rendering layer (render.py). The critical one is `test_no_none_leaks_*`: if a missing value ever renders as the literal "None", it silently poisons the model's prompt. """ from datetime import date, timedelta from rate_my_run.models import WeekStats, TrainingSummary from rate_my_run.render import format_pace, summary_to_text def _empty_weeks(n=8): """n empty weekly buckets (oldest -> newest).""" return [WeekStats(date.today() - timedelta(days=7 * (n - i)), 0, 0.0, 0.0, None) for i in range(n)] def _summary(weeks, days_since=1, mileage=None, trend="insufficient_data", signals=None): return TrainingSummary( weeks=weeks, days_since_last_run=days_since, mileage_change_pct=mileage, pace_trend=trend, signals=signals or [], ) # --- format_pace ------------------------------------------------------------ def test_format_pace_basic(): assert format_pace(5.0) == "5:00/km" assert format_pace(5.5) == "5:30/km" def test_format_pace_none(): assert format_pace(None) == "n/a" def test_format_pace_rounds_60s_up(): # 5.999 min would round to 5:60 -> must become 6:00 assert format_pace(5.999) == "6:00/km" # --- summary_to_text -------------------------------------------------------- def test_no_none_leaks_with_missing_data(): # All-empty history: pace None, mileage None, insufficient pace trend. text = summary_to_text(_summary(_empty_weeks())) assert "None" not in text def test_goal_is_included_when_given(): text = summary_to_text(_summary(_empty_weeks()), goal="run a sub-25 5K") assert "sub-25 5K" in text def test_goal_omitted_when_absent(): text = summary_to_text(_summary(_empty_weeks())) assert "stated goal" not in text def test_signals_render_as_descriptions_not_raw_keys(): text = summary_to_text(_summary(_empty_weeks(), signals=["mileage_spike"])) assert "Weekly mileage rose more than 10%" in text assert "mileage_spike" not in text # raw enum key must not leak through