f1ac56609e
Address findings from the pre-merge review: - Form (TSB) now computes only when ctl/atl are numeric (isinstance guard), so a non-numeric value can no longer raise out of format_wellness_entry and take down the entire wellness render. - The Date line uses `or` chaining so a present-but-null `date` falls back to `id` instead of rendering "Date: None". - update_wellness injects the written date into the API echo when it lacks id/date, so the confirmation body can't read "Date: N/A" under a dated header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
272 lines
8.4 KiB
Python
272 lines
8.4 KiB
Python
"""
|
|
Unit tests for formatting utilities in intervals_mcp_server.utils.formatting.
|
|
|
|
These tests verify that the formatting functions produce expected output strings for activities, workouts, wellness entries, events, and intervals.
|
|
"""
|
|
|
|
import json
|
|
from intervals_mcp_server.utils.formatting import (
|
|
format_activity_summary,
|
|
format_workout,
|
|
format_wellness_entry,
|
|
format_event_summary,
|
|
format_event_details,
|
|
format_intervals,
|
|
format_power_curves,
|
|
)
|
|
from tests.sample_data import INTERVALS_DATA
|
|
|
|
|
|
def test_format_activity_summary():
|
|
"""
|
|
Test that format_activity_summary returns a string containing the activity name and ID.
|
|
"""
|
|
data = {
|
|
"name": "Morning Ride",
|
|
"id": 1,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-01T08:00:00Z",
|
|
"distance": 1000,
|
|
"duration": 3600,
|
|
}
|
|
result = format_activity_summary(data)
|
|
assert "Activity: Morning Ride" in result
|
|
assert "ID: 1" in result
|
|
|
|
|
|
def test_format_workout():
|
|
"""
|
|
Test that format_workout returns a string containing the workout name and interval count.
|
|
"""
|
|
workout = {
|
|
"name": "Workout1",
|
|
"description": "desc",
|
|
"sport": "Ride",
|
|
"duration": 3600,
|
|
"tss": 50,
|
|
"intervals": [1, 2, 3],
|
|
}
|
|
result = format_workout(workout)
|
|
assert "Workout: Workout1" in result
|
|
assert "Intervals: 3" in result
|
|
|
|
|
|
def test_format_wellness_entry():
|
|
"""
|
|
Test that format_wellness_entry returns a string containing the date and fitness (CTL).
|
|
"""
|
|
with open("tests/ressources/wellness_entry.json", "r", encoding="utf-8") as f:
|
|
entry = json.load(f)
|
|
result = format_wellness_entry(entry)
|
|
|
|
with open("tests/ressources/wellness_entry_formatted.txt", "r", encoding="utf-8") as f:
|
|
expected_result = f.read()
|
|
assert result == expected_result
|
|
|
|
|
|
def test_format_wellness_entry_computes_form_tsb():
|
|
"""Form (TSB) is computed as CTL - ATL when both are present."""
|
|
result = format_wellness_entry({"id": "2024-06-01", "ctl": 50, "atl": 65})
|
|
assert "Form (TSB): -15.0" in result
|
|
|
|
|
|
def test_format_wellness_entry_no_form_without_both_components():
|
|
"""Form is omitted if either CTL or ATL is missing."""
|
|
result = format_wellness_entry({"id": "2024-06-01", "ctl": 50})
|
|
assert "Form (TSB)" not in result
|
|
|
|
|
|
def test_format_wellness_entry_prefers_explicit_date_over_id():
|
|
"""An explicit `date` field wins over `id` for the Date line."""
|
|
result = format_wellness_entry({"id": "2024-06-01", "date": "2024-06-02", "ctl": 50})
|
|
assert "Date: 2024-06-02" in result
|
|
|
|
|
|
def test_format_wellness_entry_null_date_falls_back_to_id():
|
|
"""A present-but-null `date` falls back to `id`, not 'None'."""
|
|
result = format_wellness_entry({"id": "2024-06-01", "date": None, "ctl": 50})
|
|
assert "Date: 2024-06-01" in result
|
|
assert "Date: None" not in result
|
|
|
|
|
|
def test_format_wellness_entry_non_numeric_ctl_atl_does_not_crash():
|
|
"""Non-numeric ctl/atl must not raise; Form is simply omitted."""
|
|
result = format_wellness_entry({"id": "2024-06-01", "ctl": "n/a", "atl": "n/a"})
|
|
assert "Form (TSB)" not in result
|
|
assert "Date: 2024-06-01" in result
|
|
|
|
|
|
def test_format_wellness_entry_include_all_fields():
|
|
"""
|
|
Test that format_wellness_entry with include_all_fields=True includes additional unknown fields.
|
|
"""
|
|
entry = {
|
|
"id": "2024-06-01",
|
|
"ctl": 80,
|
|
"weight": 75,
|
|
"customField1": "hello",
|
|
"customField2": 42,
|
|
"updated": "2024-06-01T10:00:00Z",
|
|
}
|
|
result = format_wellness_entry(entry, include_all_fields=True)
|
|
assert "Date: 2024-06-01" in result
|
|
assert "Fitness (CTL): 80" in result
|
|
assert "Weight: 75 kg" in result
|
|
assert "Other Fields:" in result
|
|
assert "customField1: hello" in result
|
|
assert "customField2: 42" in result
|
|
# "updated" is a known built-in field, should not appear in Other Fields
|
|
assert "updated:" not in result
|
|
|
|
|
|
def test_format_wellness_entry_no_extra_fields_by_default():
|
|
"""
|
|
Test that format_wellness_entry without include_all_fields does not include additional fields.
|
|
"""
|
|
entry = {
|
|
"id": "2024-06-01",
|
|
"ctl": 80,
|
|
"customField1": "hello",
|
|
}
|
|
result = format_wellness_entry(entry)
|
|
assert "Other Fields:" not in result
|
|
assert "customField1" not in result
|
|
|
|
|
|
def test_format_wellness_entry_macros_populated():
|
|
"""
|
|
Test that format_wellness_entry renders native nutrition macros
|
|
(carbohydrates, protein, fatTotal) in grams when present.
|
|
"""
|
|
entry = {
|
|
"id": "2026-04-08",
|
|
"carbohydrates": 310,
|
|
"protein": 145,
|
|
"fatTotal": 72,
|
|
}
|
|
result = format_wellness_entry(entry)
|
|
assert "Nutrition & Hydration:" in result
|
|
assert "- Carbohydrates: 310 g" in result
|
|
assert "- Protein: 145 g" in result
|
|
assert "- Fat: 72 g" in result
|
|
|
|
|
|
def test_format_wellness_entry_macros_null_hidden():
|
|
"""
|
|
Test that format_wellness_entry hides macro lines when the fields are null,
|
|
preserving backward compatibility with older wellness records.
|
|
"""
|
|
entry = {
|
|
"id": "2026-04-08",
|
|
"ctl": 80,
|
|
"carbohydrates": None,
|
|
"protein": None,
|
|
"fatTotal": None,
|
|
}
|
|
result = format_wellness_entry(entry)
|
|
assert "Carbohydrates" not in result
|
|
assert "Protein" not in result
|
|
# "Fat" could legitimately appear inside e.g. "Body Fat" elsewhere, so
|
|
# anchor the negative assertion on the line-prefix form we would emit.
|
|
assert "- Fat:" not in result
|
|
|
|
|
|
def test_format_event_summary():
|
|
"""
|
|
Test that format_event_summary returns a string containing the event date and type.
|
|
"""
|
|
event = {
|
|
"start_date_local": "2024-01-01",
|
|
"id": "e1",
|
|
"name": "Event1",
|
|
"description": "desc",
|
|
"race": True,
|
|
}
|
|
summary = format_event_summary(event)
|
|
assert "Date: 2024-01-01" in summary
|
|
assert "Type: Race" in summary
|
|
|
|
|
|
def test_format_event_details():
|
|
"""
|
|
Test that format_event_details returns a string containing event and workout details.
|
|
"""
|
|
event = {
|
|
"id": "e1",
|
|
"date": "2024-01-01",
|
|
"name": "Event1",
|
|
"description": "desc",
|
|
"workout": {
|
|
"id": "w1",
|
|
"sport": "Ride",
|
|
"duration": 3600,
|
|
"tss": 50,
|
|
"intervals": [1, 2],
|
|
},
|
|
"race": True,
|
|
"priority": "A",
|
|
"result": "1st",
|
|
"calendar": {"name": "Main"},
|
|
}
|
|
details = format_event_details(event)
|
|
assert "Event Details:" in details
|
|
assert "Workout Information:" in details
|
|
|
|
|
|
def test_format_intervals():
|
|
"""
|
|
Test that format_intervals returns a string containing interval analysis and the interval label.
|
|
"""
|
|
result = format_intervals(INTERVALS_DATA)
|
|
assert "Intervals Analysis:" in result
|
|
assert "Rep 1" in result
|
|
|
|
|
|
def test_format_power_curves():
|
|
"""
|
|
Test that format_power_curves returns a concise string with curve labels,
|
|
power values, W/kg values, and activity IDs.
|
|
"""
|
|
curves = [
|
|
{
|
|
"id": "s0",
|
|
"label": "This season",
|
|
"start": "2025-09-29T00:00:00",
|
|
"end": "2026-03-14T00:00:00",
|
|
"data_points": [
|
|
{"secs": 5, "watts": 780, "activity_id": "i100", "watts_per_kg": 10.4, "wkg_activity_id": "i100"},
|
|
{"secs": 60, "watts": 380, "activity_id": "i102", "watts_per_kg": 5.07, "wkg_activity_id": "i102"},
|
|
{"secs": 3600, "watts": 210, "activity_id": "i107", "watts_per_kg": 2.8, "wkg_activity_id": "i107"},
|
|
],
|
|
},
|
|
]
|
|
result = format_power_curves(curves, "Ride", include_normalised=True)
|
|
assert "Power Curves (Ride):" in result
|
|
assert "This season" in result
|
|
assert "5s: 780W" in result
|
|
assert "10.40W/kg" in result
|
|
assert "1m: 380W" in result
|
|
assert "1h: 210W" in result
|
|
assert "i100" in result
|
|
assert "i107" in result
|
|
|
|
|
|
def test_format_power_curves_without_normalised():
|
|
"""
|
|
Test that format_power_curves without normalised data does not include W/kg.
|
|
"""
|
|
curves = [
|
|
{
|
|
"id": "s0",
|
|
"label": "This season",
|
|
"start": "",
|
|
"end": "",
|
|
"data_points": [
|
|
{"secs": 5, "watts": 780, "activity_id": "i100"},
|
|
],
|
|
},
|
|
]
|
|
result = format_power_curves(curves, "Ride", include_normalised=False)
|
|
assert "780W" in result
|
|
assert "W/kg" not in result
|