ba441aa1ee
Remove unused imports (F401) and strip trailing whitespace (W291/W293) flagged by ruff in test files. Pre-existing debt unrelated to any single feature; CI runs pytest but not ruff, so these had accumulated. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
952 lines
30 KiB
Python
952 lines
30 KiB
Python
"""
|
|
Unit tests for the main MCP server tool functions in intervals_mcp_server.server.
|
|
|
|
These tests use monkeypatching to mock API responses and verify the formatting and output of each tool function:
|
|
- get_activities
|
|
- get_activity_details
|
|
- get_activity_intervals
|
|
- get_activity_streams
|
|
- get_activity_messages
|
|
- add_activity_message
|
|
- get_events
|
|
- get_event_by_id
|
|
- add_or_update_event
|
|
- get_wellness_data
|
|
|
|
The tests ensure that the server's public API returns expected strings and handles data correctly.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src"))
|
|
os.environ.setdefault("API_KEY", "test")
|
|
os.environ.setdefault("ATHLETE_ID", "i1")
|
|
|
|
from intervals_mcp_server.server import ( # pylint: disable=wrong-import-position
|
|
add_activity_message,
|
|
get_activities,
|
|
get_activity_details,
|
|
get_activity_intervals,
|
|
get_activity_messages,
|
|
get_activity_streams,
|
|
add_or_update_event,
|
|
get_athlete_power_curves,
|
|
get_event_by_id,
|
|
get_events,
|
|
get_gear_list,
|
|
get_wellness_data,
|
|
get_custom_items,
|
|
get_custom_item_by_id,
|
|
create_custom_item,
|
|
update_custom_item,
|
|
delete_custom_item,
|
|
)
|
|
from intervals_mcp_server.tools import gear as gear_module # pylint: disable=wrong-import-position
|
|
from tests.sample_data import INTERVALS_DATA, POWER_CURVES_DATA # pylint: disable=wrong-import-position
|
|
|
|
|
|
def _reset_gear_cache():
|
|
"""Helper to clear the module-level gear cache between tests."""
|
|
gear_module._GEAR_RAW_CACHE.clear() # pylint: disable=protected-access
|
|
|
|
|
|
def test_get_activities(monkeypatch):
|
|
"""
|
|
Test get_activities returns a formatted string containing activity details when given a sample activity.
|
|
"""
|
|
sample = {
|
|
"name": "Morning Ride",
|
|
"id": 123,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-01T08:00:00Z",
|
|
"distance": 1000,
|
|
"duration": 3600,
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return [sample]
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activities(limit=1, include_unnamed=True))
|
|
assert "Morning Ride" in result
|
|
assert "Activities:" in result
|
|
|
|
|
|
def test_get_activity_details(monkeypatch):
|
|
"""
|
|
Test get_activity_details returns a formatted string with the activity name and details.
|
|
"""
|
|
sample = {
|
|
"name": "Morning Ride",
|
|
"id": 123,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-01T08:00:00Z",
|
|
"distance": 1000,
|
|
"duration": 3600,
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return sample
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_details(123))
|
|
assert "Activity: Morning Ride" in result
|
|
|
|
|
|
def test_get_events(monkeypatch):
|
|
"""
|
|
Test get_events returns a formatted string containing event details when given a sample event.
|
|
"""
|
|
event = {
|
|
"date": "2024-01-01",
|
|
"id": "e1",
|
|
"name": "Test Event",
|
|
"description": "desc",
|
|
"race": True,
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return [event]
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr("intervals_mcp_server.tools.events.make_intervals_request", fake_request)
|
|
result = asyncio.run(get_events(start_date="2024-01-01", end_date="2024-01-02"))
|
|
assert "Test Event" in result
|
|
assert "Events:" in result
|
|
|
|
|
|
def test_get_event_by_id(monkeypatch):
|
|
"""
|
|
Test get_event_by_id returns a formatted string with event details for a given event ID.
|
|
"""
|
|
event = {
|
|
"id": "e1",
|
|
"date": "2024-01-01",
|
|
"name": "Test Event",
|
|
"description": "desc",
|
|
"race": True,
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return event
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr("intervals_mcp_server.tools.events.make_intervals_request", fake_request)
|
|
result = asyncio.run(get_event_by_id("e1"))
|
|
assert "Event Details:" in result
|
|
assert "Test Event" in result
|
|
|
|
|
|
def test_get_wellness_data(monkeypatch):
|
|
"""
|
|
Test get_wellness_data returns a formatted string containing wellness data for a given athlete.
|
|
"""
|
|
wellness = {
|
|
"2024-01-01": {
|
|
"id": "2024-01-01",
|
|
"ctl": 75,
|
|
"sleepSecs": 28800,
|
|
}
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return wellness
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
|
|
result = asyncio.run(get_wellness_data())
|
|
assert "Wellness Data:" in result
|
|
assert "2024-01-01" in result
|
|
|
|
|
|
def test_get_wellness_data_renders_macros(monkeypatch):
|
|
"""
|
|
Integration test: native nutrition macros (carbohydrates, protein,
|
|
fatTotal) flow from the API response through get_wellness_data into the
|
|
formatted output.
|
|
"""
|
|
wellness = [
|
|
{
|
|
"id": "2026-04-08",
|
|
"carbohydrates": 310,
|
|
"protein": 145,
|
|
"fatTotal": 72,
|
|
}
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return wellness
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
|
|
result = asyncio.run(get_wellness_data())
|
|
assert "Wellness Data:" in result
|
|
assert "2026-04-08" in result
|
|
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_get_wellness_data_include_all_fields(monkeypatch):
|
|
"""
|
|
Test get_wellness_data with include_all_fields=True returns a formatted string including additional fields.
|
|
"""
|
|
wellness = [
|
|
{
|
|
"id": "2024-01-01",
|
|
"ctl": 75,
|
|
"sleepSecs": 28800,
|
|
"customField": "custom_value",
|
|
}
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return wellness
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
|
|
result = asyncio.run(get_wellness_data(include_all_fields=True))
|
|
assert "Wellness Data:" in result
|
|
assert "2024-01-01" in result
|
|
assert "Fitness (CTL): 75" in result
|
|
assert "Other Fields:" in result
|
|
assert "customField: custom_value" in result
|
|
|
|
|
|
def test_get_activity_intervals(monkeypatch):
|
|
"""
|
|
Test get_activity_intervals returns a formatted string with interval analysis for a given activity.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return INTERVALS_DATA
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_intervals("123"))
|
|
assert "Intervals Analysis:" in result
|
|
assert "Rep 1" in result
|
|
|
|
|
|
def test_get_activity_streams(monkeypatch):
|
|
"""
|
|
Test get_activity_streams returns a formatted string with stream data for a given activity.
|
|
"""
|
|
sample_streams = [
|
|
{
|
|
"type": "time",
|
|
"name": "time",
|
|
"data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
"data2": [],
|
|
"valueType": "time_units",
|
|
"valueTypeIsArray": False,
|
|
"anomalies": None,
|
|
"custom": False,
|
|
},
|
|
{
|
|
"type": "watts",
|
|
"name": "watts",
|
|
"data": [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200],
|
|
"data2": [],
|
|
"valueType": "power_units",
|
|
"valueTypeIsArray": False,
|
|
"anomalies": None,
|
|
"custom": False,
|
|
},
|
|
{
|
|
"type": "heartrate",
|
|
"name": "heartrate",
|
|
"data": [120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170],
|
|
"data2": [],
|
|
"valueType": "hr_units",
|
|
"valueTypeIsArray": False,
|
|
"anomalies": None,
|
|
"custom": False,
|
|
},
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return sample_streams
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_streams("i107537962"))
|
|
assert "Activity Streams" in result
|
|
assert "time" in result
|
|
assert "watts" in result
|
|
assert "heartrate" in result
|
|
assert "Data Points: 11" in result
|
|
|
|
|
|
def test_add_or_update_event(monkeypatch):
|
|
"""
|
|
Test add_or_update_event successfully posts an event and returns the response data.
|
|
"""
|
|
expected_response = {
|
|
"id": "e123",
|
|
"start_date_local": "2024-01-15T00:00:00",
|
|
"category": "WORKOUT",
|
|
"name": "Test Workout",
|
|
"type": "Ride",
|
|
}
|
|
|
|
async def fake_post_request(*_args, **_kwargs):
|
|
return expected_response
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_post_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.events.make_intervals_request", fake_post_request
|
|
)
|
|
result = asyncio.run(
|
|
add_or_update_event(
|
|
start_date="2024-01-15", name="Test Workout", workout_type="Ride"
|
|
)
|
|
)
|
|
assert "Successfully created event id:" in result
|
|
assert "e123" in result
|
|
|
|
|
|
def test_get_activity_messages(monkeypatch):
|
|
"""Test get_activity_messages returns formatted messages for an activity."""
|
|
sample_messages = [
|
|
{
|
|
"id": 1,
|
|
"name": "Niko",
|
|
"created": "2024-06-15T10:30:00Z",
|
|
"type": "NOTE",
|
|
"content": "Legs felt heavy today",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Coach",
|
|
"created": "2024-06-15T11:00:00Z",
|
|
"type": "TEXT",
|
|
"content": "Good effort despite that!",
|
|
},
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return sample_messages
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_messages(activity_id="i123"))
|
|
assert "Legs felt heavy today" in result
|
|
assert "Good effort despite that!" in result
|
|
assert "Niko" in result
|
|
assert "Coach" in result
|
|
|
|
|
|
def test_get_activity_messages_error(monkeypatch):
|
|
"""Test get_activity_messages handles API errors gracefully."""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return {"error": True, "message": "Activity not found"}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_messages(activity_id="i999"))
|
|
assert "Error fetching activity messages" in result
|
|
assert "Activity not found" in result
|
|
|
|
|
|
def test_get_activity_messages_empty(monkeypatch):
|
|
"""Test get_activity_messages returns appropriate message when no messages exist."""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return []
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_activity_messages(activity_id="i123"))
|
|
assert "No messages found" in result
|
|
|
|
|
|
def test_add_activity_message(monkeypatch):
|
|
"""Test add_activity_message posts a message and returns confirmation."""
|
|
|
|
async def fake_request(*_args, **kwargs):
|
|
assert kwargs.get("method") == "POST"
|
|
assert kwargs.get("data") == {"content": "Great run!"}
|
|
return {"id": 42, "new_chat": None}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(add_activity_message(activity_id="i123", content="Great run!"))
|
|
assert "Successfully added message" in result
|
|
assert "42" in result
|
|
|
|
|
|
def test_add_activity_message_missing_id(monkeypatch):
|
|
"""Test add_activity_message warns when response has no ID."""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return {"new_chat": None}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(add_activity_message(activity_id="i123", content="Hello"))
|
|
assert "appears to have been added" in result
|
|
assert "verify manually" in result
|
|
|
|
|
|
def test_add_activity_message_unexpected_response(monkeypatch):
|
|
"""Test add_activity_message handles unexpected non-dict response."""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return None
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(add_activity_message(activity_id="i123", content="Hello"))
|
|
assert "Unexpected response" in result
|
|
|
|
|
|
def test_add_activity_message_error(monkeypatch):
|
|
"""Test add_activity_message handles API errors."""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return {"error": True, "message": "Not found"}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(add_activity_message(activity_id="i999", content="Hello"))
|
|
assert "Error adding message" in result
|
|
|
|
|
|
def test_get_athlete_power_curves(monkeypatch):
|
|
"""
|
|
Test get_athlete_power_curves returns formatted power curve data with both seasons.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return POWER_CURVES_DATA
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
get_athlete_power_curves(
|
|
activity_type="Ride",
|
|
|
|
)
|
|
)
|
|
assert "Power Curves (Ride):" in result
|
|
assert "This season" in result
|
|
assert "Last season" in result
|
|
assert "5s:" in result
|
|
assert "W/kg" in result
|
|
assert "i100" in result
|
|
|
|
|
|
def test_get_athlete_power_curves_custom_durations(monkeypatch):
|
|
"""
|
|
Test get_athlete_power_curves with custom durations returns only those durations.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return POWER_CURVES_DATA
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
get_athlete_power_curves(
|
|
activity_type="Ride",
|
|
durations=[5, 60],
|
|
|
|
)
|
|
)
|
|
assert "5s:" in result
|
|
assert "1m:" in result
|
|
# Should not contain durations we didn't request
|
|
assert "15s:" not in result
|
|
assert "10m:" not in result
|
|
|
|
|
|
def test_get_athlete_power_curves_without_normalised(monkeypatch):
|
|
"""
|
|
Test get_athlete_power_curves without normalised data excludes W/kg values.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return POWER_CURVES_DATA
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
get_athlete_power_curves(
|
|
activity_type="Ride",
|
|
include_normalised=False,
|
|
|
|
)
|
|
)
|
|
assert "W/kg" not in result
|
|
assert "780W" in result
|
|
|
|
|
|
def test_get_athlete_power_curves_date_validation(monkeypatch):
|
|
"""
|
|
Test get_athlete_power_curves validates date parameters.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return POWER_CURVES_DATA
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
|
|
)
|
|
# Only start_date without end_date should fail
|
|
result = asyncio.run(
|
|
get_athlete_power_curves(
|
|
activity_type="Ride",
|
|
start_date="2026-01-01",
|
|
|
|
)
|
|
)
|
|
assert "Error" in result
|
|
assert "start_date and end_date must be provided together" in result
|
|
|
|
|
|
def test_get_athlete_power_curves_no_curves_selected(monkeypatch):
|
|
"""
|
|
Test get_athlete_power_curves returns error when no curves selected.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return POWER_CURVES_DATA
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
get_athlete_power_curves(
|
|
activity_type="Ride",
|
|
this_season=False,
|
|
last_season=False,
|
|
|
|
)
|
|
)
|
|
assert "Error" in result
|
|
assert "At least one curve must be selected" in result
|
|
|
|
|
|
def test_get_custom_items(monkeypatch):
|
|
"""
|
|
Test get_custom_items returns a formatted string containing custom item details.
|
|
"""
|
|
custom_items = [
|
|
{"id": 1, "name": "HR Zones", "type": "ZONES", "description": "Heart rate zones"},
|
|
{"id": 2, "name": "Power Chart", "type": "FITNESS_CHART", "description": None},
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return custom_items
|
|
|
|
# Patch in both api.client and tools modules to ensure it works
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_custom_items())
|
|
assert "Custom Items:" in result
|
|
assert "HR Zones" in result
|
|
assert "ZONES" in result
|
|
assert "Power Chart" in result
|
|
|
|
|
|
def test_get_custom_item_by_id(monkeypatch):
|
|
"""
|
|
Test get_custom_item_by_id returns formatted details of a single custom item.
|
|
"""
|
|
custom_item = {
|
|
"id": 1,
|
|
"name": "HR Zones",
|
|
"type": "ZONES",
|
|
"description": "Heart rate zones",
|
|
"visibility": "PRIVATE",
|
|
"index": 0,
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return custom_item
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(get_custom_item_by_id(item_id=1))
|
|
assert "Custom Item Details:" in result
|
|
assert "HR Zones" in result
|
|
assert "ZONES" in result
|
|
assert "Heart rate zones" in result
|
|
assert "PRIVATE" in result
|
|
|
|
|
|
def test_create_custom_item(monkeypatch):
|
|
"""
|
|
Test create_custom_item returns a success message with formatted item details.
|
|
"""
|
|
created_item = {
|
|
"id": 10,
|
|
"name": "New Chart",
|
|
"type": "FITNESS_CHART",
|
|
"description": "A new fitness chart",
|
|
"visibility": "PRIVATE",
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return created_item
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
create_custom_item(name="New Chart", item_type="FITNESS_CHART")
|
|
)
|
|
assert "Successfully created custom item:" in result
|
|
assert "New Chart" in result
|
|
assert "FITNESS_CHART" in result
|
|
|
|
|
|
def test_create_custom_item_with_string_content(monkeypatch):
|
|
"""
|
|
Test create_custom_item correctly parses content when passed as a JSON string.
|
|
"""
|
|
captured: dict = {}
|
|
|
|
async def fake_request(*_args, **kwargs):
|
|
captured["data"] = kwargs.get("data")
|
|
return {
|
|
"id": 11,
|
|
"name": "Activity Field",
|
|
"type": "ACTIVITY_FIELD",
|
|
"content": {"expression": "icu_training_load"},
|
|
}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
create_custom_item(
|
|
name="Activity Field",
|
|
item_type="ACTIVITY_FIELD",
|
|
|
|
content='{"expression": "icu_training_load"}', # type: ignore[arg-type]
|
|
)
|
|
)
|
|
assert "Successfully created custom item:" in result
|
|
# Verify the content was parsed from string to dict before being sent
|
|
assert isinstance(captured["data"]["content"], dict)
|
|
assert captured["data"]["content"]["expression"] == "icu_training_load"
|
|
|
|
|
|
def test_update_custom_item(monkeypatch):
|
|
"""
|
|
Test update_custom_item returns a success message with formatted item details.
|
|
"""
|
|
updated_item = {
|
|
"id": 1,
|
|
"name": "Updated Chart",
|
|
"type": "FITNESS_CHART",
|
|
"description": "Updated description",
|
|
"visibility": "PUBLIC",
|
|
}
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return updated_item
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
update_custom_item(item_id=1, name="Updated Chart")
|
|
)
|
|
assert "Successfully updated custom item:" in result
|
|
assert "Updated Chart" in result
|
|
assert "PUBLIC" in result
|
|
|
|
|
|
def test_delete_custom_item(monkeypatch):
|
|
"""
|
|
Test delete_custom_item returns the API response.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return {}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(delete_custom_item(item_id=1))
|
|
assert "Successfully deleted" in result
|
|
|
|
|
|
def test_create_custom_item_with_invalid_json_content(monkeypatch):
|
|
"""
|
|
Test create_custom_item returns an error message when content is an invalid JSON string.
|
|
"""
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return {}
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
|
|
)
|
|
result = asyncio.run(
|
|
create_custom_item(
|
|
name="Bad Item",
|
|
item_type="FITNESS_CHART",
|
|
|
|
content="not valid json", # type: ignore[arg-type]
|
|
)
|
|
)
|
|
assert "Error: content must be valid JSON when passed as a string." in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gear tools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_gear_list(monkeypatch):
|
|
"""
|
|
Test get_gear_list returns a formatted catalog with id, type, name and stats.
|
|
"""
|
|
_reset_gear_cache()
|
|
|
|
sample_gear = [
|
|
{
|
|
"id": "b1",
|
|
"type": "Bike",
|
|
"name": "Litening Air",
|
|
"default_for_type": "Ride",
|
|
"activities": 100,
|
|
"distance": 4_155_700,
|
|
"retired": False,
|
|
},
|
|
{
|
|
"id": "b2",
|
|
"type": "Bike",
|
|
"name": "Retired bike",
|
|
"activities": 50,
|
|
"distance": 2_000_000,
|
|
"retired": True,
|
|
},
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return sample_gear
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
|
|
)
|
|
|
|
result = asyncio.run(get_gear_list())
|
|
|
|
assert "Gear catalog for athlete i1:" in result
|
|
assert "Litening Air" in result
|
|
assert "b1" in result
|
|
assert "Retired bike" in result
|
|
assert "yes" in result # retired flag rendered
|
|
assert "Ride" in result # default_for_type rendered
|
|
|
|
|
|
def test_get_gear_list_empty(monkeypatch):
|
|
"""
|
|
Test get_gear_list returns an informative message when no gear is configured.
|
|
"""
|
|
_reset_gear_cache()
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
return []
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
|
|
)
|
|
|
|
result = asyncio.run(get_gear_list())
|
|
assert "No gear found" in result
|
|
|
|
|
|
def test_get_gear_list_cache_and_refresh(monkeypatch):
|
|
"""
|
|
Test that get_gear_list caches the catalog and that refresh=True busts the cache.
|
|
"""
|
|
_reset_gear_cache()
|
|
|
|
call_count = {"n": 0}
|
|
sample_gear = [
|
|
{
|
|
"id": "b1",
|
|
"type": "Bike",
|
|
"name": "Litening Air",
|
|
"activities": 100,
|
|
"distance": 4_155_700,
|
|
}
|
|
]
|
|
|
|
async def fake_request(*_args, **_kwargs):
|
|
call_count["n"] += 1
|
|
return sample_gear
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
|
|
)
|
|
|
|
# First call: cache cold, one API hit expected.
|
|
asyncio.run(get_gear_list())
|
|
assert call_count["n"] == 1
|
|
|
|
# Second call: cache warm, no additional API hit.
|
|
asyncio.run(get_gear_list())
|
|
assert call_count["n"] == 1
|
|
|
|
# refresh=True busts the cache and triggers a fresh fetch.
|
|
asyncio.run(get_gear_list(refresh=True))
|
|
assert call_count["n"] == 2
|
|
|
|
|
|
def test_get_activity_details_resolves_gear_name(monkeypatch):
|
|
"""
|
|
Test get_activity_details injects the resolved gear name into the formatted output
|
|
when the activity payload contains a gear_id.
|
|
"""
|
|
_reset_gear_cache()
|
|
|
|
activity = {
|
|
"name": "Morning Ride",
|
|
"id": 123,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-01T08:00:00Z",
|
|
"distance": 1000,
|
|
"duration": 3600,
|
|
"gear_id": "b1",
|
|
}
|
|
gear_catalog = [{"id": "b1", "type": "Bike", "name": "Litening Air"}]
|
|
|
|
async def fake_request(url=None, **_kwargs):
|
|
# The activity endpoint and the gear endpoint share the same fake
|
|
# request; route by URL pattern.
|
|
if url and "/gear" in url:
|
|
return gear_catalog
|
|
return activity
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
|
|
)
|
|
# get_activity_details does not accept athlete_id; gear resolution falls
|
|
# back to the configured ATHLETE_ID, which is unset under CI. Provide one.
|
|
monkeypatch.setattr(gear_module.config, "athlete_id", "1")
|
|
|
|
result = asyncio.run(get_activity_details(123))
|
|
assert "Activity: Morning Ride" in result
|
|
assert "Gear:" in result
|
|
assert "Name: Litening Air" in result
|
|
assert "ID: b1" in result
|
|
|
|
|
|
def test_get_activities_resolves_gear_name(monkeypatch):
|
|
"""
|
|
Test get_activities injects resolved gear names for each activity in the list.
|
|
"""
|
|
_reset_gear_cache()
|
|
|
|
activities = [
|
|
{
|
|
"name": "Ride 1",
|
|
"id": 1,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-01T08:00:00Z",
|
|
"distance": 1000,
|
|
"duration": 3600,
|
|
"gear_id": "b1",
|
|
},
|
|
{
|
|
"name": "Ride 2",
|
|
"id": 2,
|
|
"type": "Ride",
|
|
"startTime": "2024-01-02T08:00:00Z",
|
|
"distance": 2000,
|
|
"duration": 5400,
|
|
"gear_id": "b2",
|
|
},
|
|
]
|
|
gear_catalog = [
|
|
{"id": "b1", "type": "Bike", "name": "Litening Air"},
|
|
{"id": "b2", "type": "Bike", "name": "S-Works Tarmac SL8"},
|
|
]
|
|
|
|
async def fake_request(url=None, **_kwargs):
|
|
if url and "/gear" in url:
|
|
return gear_catalog
|
|
return activities
|
|
|
|
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
|
|
)
|
|
monkeypatch.setattr(
|
|
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
|
|
)
|
|
|
|
result = asyncio.run(get_activities(limit=2, include_unnamed=True))
|
|
assert "Ride 1" in result
|
|
assert "Ride 2" in result
|
|
assert "Name: Litening Air" in result
|
|
assert "Name: S-Works Tarmac SL8" in result
|