diff --git a/backend/internal/garmin/pyscript/tests/test_wrapper.py b/backend/internal/garmin/pyscript/tests/test_wrapper.py index 6e3fe67..3d20cc3 100644 --- a/backend/internal/garmin/pyscript/tests/test_wrapper.py +++ b/backend/internal/garmin/pyscript/tests/test_wrapper.py @@ -269,3 +269,25 @@ def test_authenticate_prevents_later_lazy_startup_login(): wrapper.dispatch({"id": 34, "cmd": "call", "params": {"method": "x", "args": {}}}) mock_startup.assert_not_called() + + +def test_call_marks_not_found_error_specifically(): + from garminconnect import GarminConnectNotFoundError + + wrapper._auth_state = "authenticated" + wrapper._client = MagicMock() + wrapper._client.get_workout_by_id.side_effect = GarminConnectNotFoundError("API Error 404") + resp = wrapper.dispatch({ + "id": 11, "cmd": "call", "params": {"method": "get_workout_by_id", "args": {"workout_id": "999"}}, + }) + assert resp == {"id": 11, "error": "API Error 404", "not_found": True} + + +def test_call_does_not_mark_other_errors_as_not_found(): + wrapper._auth_state = "authenticated" + wrapper._client = MagicMock() + wrapper._client.get_workout_by_id.side_effect = Exception("rate limited") + resp = wrapper.dispatch({ + "id": 12, "cmd": "call", "params": {"method": "get_workout_by_id", "args": {"workout_id": "999"}}, + }) + assert resp == {"id": 12, "error": "rate limited"} diff --git a/backend/internal/garmin/pyscript/wrapper.py b/backend/internal/garmin/pyscript/wrapper.py index fc0e823..074f024 100644 --- a/backend/internal/garmin/pyscript/wrapper.py +++ b/backend/internal/garmin/pyscript/wrapper.py @@ -9,7 +9,7 @@ import sys import threading import traceback -from garminconnect import Garmin +from garminconnect import Garmin, GarminConnectNotFoundError TOKENSTORE = os.path.expanduser(os.environ.get("GARMIN_TOKENSTORE", "~/.garmin")) @@ -208,7 +208,15 @@ def dispatch(req): except Exception as exc: _debug(f"{req.get('cmd')} raised {type(exc).__name__}: {exc}") _debug(traceback.format_exc()) - return {"id": req.get("id"), "error": str(exc)} + resp = {"id": req.get("id"), "error": str(exc)} + # A 404 (e.g. get_workout_by_id for a workout deleted on Garmin's + # side after being linked to an activity) is definitive, not a + # transient failure worth retrying forever -- marked specifically so + # internal/garmin/client.go can tell the two apart (see + # docs/superpowers/specs/2026-07-27-workout-not-found-design.md). + if isinstance(exc, GarminConnectNotFoundError): + resp["not_found"] = True + return resp def main():