fix: bound startup-login timeout, refresh stale SKILL.md and doc comments
_startup_login() ran garminconnect's login synchronously with no timeout before main() ever started reading stdin, so a slow/rate-limited Garmin login could wedge a user's whole subprocess before it became responsive. Give it the same background-thread + bounded-10s-timeout shape _handle_authenticate already uses, with tests for both the fast-success and timeout paths. Also refreshes .claude/skills/geniusrun-dev/SKILL.md (still describing the retired mcp-garmin MCP architecture) and three stale doc comments (garmin.AuthStatus, config.GarminTokenStoreRoot, mock package doc) left over from the direct-wrapper migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,3 +124,35 @@ def test_dispatch_unknown_cmd_is_error():
|
||||
resp = wrapper.dispatch({"id": 10, "cmd": "not_a_real_cmd"})
|
||||
assert resp["id"] == 10
|
||||
assert "unknown cmd" in resp["error"]
|
||||
|
||||
|
||||
def test_startup_login_success():
|
||||
env = {"GARMIN_EMAIL": "test@example.com", "GARMIN_PASSWORD": "secret"}
|
||||
with patch.dict("os.environ", env):
|
||||
with patch("wrapper.Garmin") as mock_garmin_cls:
|
||||
mock_garmin_cls.return_value = MagicMock()
|
||||
wrapper._startup_login()
|
||||
assert wrapper._auth_state == "authenticated"
|
||||
|
||||
|
||||
def test_startup_login_missing_credentials_returns_immediately():
|
||||
with patch.dict("os.environ", {}, clear=False):
|
||||
os.environ.pop("GARMIN_EMAIL", None)
|
||||
os.environ.pop("GARMIN_PASSWORD", None)
|
||||
wrapper._auth_state = "unauthenticated"
|
||||
wrapper._startup_login()
|
||||
assert wrapper._auth_state == "unauthenticated"
|
||||
assert wrapper._client is None
|
||||
|
||||
|
||||
def test_startup_login_times_out_without_blocking():
|
||||
env = {"GARMIN_EMAIL": "test@example.com", "GARMIN_PASSWORD": "secret"}
|
||||
with patch.dict("os.environ", env):
|
||||
with patch("wrapper.Garmin") as mock_garmin_cls:
|
||||
mock_garmin_cls.return_value = MagicMock()
|
||||
with patch.object(wrapper._login_result_queue, "get", side_effect=queue.Empty):
|
||||
# Should return promptly (bounded by the 10s timeout passed to
|
||||
# queue.get, which is mocked here to raise immediately) rather
|
||||
# than blocking main()'s stdin dispatch loop from starting.
|
||||
wrapper._startup_login()
|
||||
assert wrapper._auth_state == "unauthenticated"
|
||||
|
||||
@@ -33,18 +33,45 @@ def _prompt_mfa():
|
||||
def _startup_login():
|
||||
"""Silently resume a cached tokenstore session at process start, so a
|
||||
freshly (re)spawned subprocess is already authenticated for background
|
||||
syncs that never call the explicit authenticate command."""
|
||||
syncs that never call the explicit authenticate command.
|
||||
|
||||
Bounded to the same 10s timeout as _handle_authenticate: the actual
|
||||
login runs on a background daemon thread, and this function waits up
|
||||
to 10s for it before returning either way. This runs before main()
|
||||
enters its stdin dispatch loop, so a slow/rate-limited Garmin login
|
||||
must never block indefinitely here -- if it times out, the thread
|
||||
keeps running and will update _auth_state whenever it eventually
|
||||
finishes (success or failure), same as today, just without wedging
|
||||
the whole subprocess unresponsive in the meantime."""
|
||||
global _client, _auth_state
|
||||
email = os.environ.get("GARMIN_EMAIL")
|
||||
password = os.environ.get("GARMIN_PASSWORD")
|
||||
if not email or not password:
|
||||
return
|
||||
|
||||
_client = Garmin(email, password)
|
||||
|
||||
def _do_login():
|
||||
try:
|
||||
_client.login(tokenstore=TOKENSTORE)
|
||||
_login_result_queue.put(("success", None))
|
||||
except Exception as exc:
|
||||
_login_result_queue.put(("error", str(exc)))
|
||||
|
||||
threading.Thread(target=_do_login, daemon=True).start()
|
||||
|
||||
try:
|
||||
_client = Garmin(email, password)
|
||||
_client.login(tokenstore=TOKENSTORE)
|
||||
_auth_state = "authenticated"
|
||||
except Exception as exc:
|
||||
_debug(f"startup tokenstore login failed: {exc}")
|
||||
status, err = _login_result_queue.get(timeout=10)
|
||||
if status == "success":
|
||||
_auth_state = "authenticated"
|
||||
else:
|
||||
_debug(f"startup tokenstore login failed: {err}")
|
||||
_auth_state = "unauthenticated"
|
||||
except queue.Empty:
|
||||
_debug(
|
||||
"startup tokenstore login hit the 10s timeout -- still running in the "
|
||||
"background and will update auth state whenever it finishes"
|
||||
)
|
||||
_auth_state = "unauthenticated"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user