From 840fb55a8aaeb69bfcd6f34a80e57f9a5bcd44ce Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:22:55 +0530 Subject: [PATCH] fix(auth): enrich device-auth timeout at the source to cover the dashboard poller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /simplify-code review found _poll_for_token has a second caller: web_server._nous_poller (dashboard/desktop device login), which surfaces str(e) as the UI error_message — so wrapping only in _nous_device_code_login left the dashboard showing the bare timeout. Move the enrichment into _poll_for_token's deadline raise so every caller inherits the guidance, and drop the now-redundant try/except wrap in the CLI login. Add a source-level regression test driving the real poll loop (authorization_pending stub client) to the deadline. --- hermes_cli/auth.py | 26 ++++++------ tests/hermes_cli/test_auth_nous_provider.py | 45 ++++++++++++++++++++- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 2ed16339d437b..c08c973045e81 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -5123,7 +5123,10 @@ def _poll_for_token( description = error_payload.get("error_description") or "Unknown authentication error" raise RuntimeError(f"{error_code}: {description}") - raise TimeoutError("Timed out waiting for device authorization") + # Enriched at the SOURCE so every caller inherits the guidance: + # the CLI login (_nous_device_code_login) and the dashboard/desktop + # poller (web_server._nous_poller, which surfaces str(e) to the UI). + raise TimeoutError(_nous_device_auth_timeout_message(portal_base_url)) # ============================================================================= @@ -8631,19 +8634,14 @@ def _nous_device_code_login( effective_interval = max(1, min(interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS)) print(f"Waiting for approval (polling every {effective_interval}s)...") - try: - token_data = _poll_for_token( - client=client, - portal_base_url=portal_base_url, - client_id=client_id, - device_code=str(device_data["device_code"]), - expires_in=expires_in, - poll_interval=interval, - ) - except TimeoutError as exc: - raise TimeoutError( - _nous_device_auth_timeout_message(portal_base_url) - ) from exc + token_data = _poll_for_token( + client=client, + portal_base_url=portal_base_url, + client_id=client_id, + device_code=str(device_data["device_code"]), + expires_in=expires_in, + poll_interval=interval, + ) now = datetime.now(timezone.utc) token_expires_in = _coerce_ttl_seconds(token_data.get("expires_in", 0)) diff --git a/tests/hermes_cli/test_auth_nous_provider.py b/tests/hermes_cli/test_auth_nous_provider.py index 55f432554f2e9..a3be5053d0b33 100644 --- a/tests/hermes_cli/test_auth_nous_provider.py +++ b/tests/hermes_cli/test_auth_nous_provider.py @@ -1092,8 +1092,45 @@ class TestNousDeviceAuthTimeoutMessage: assert f"{DEFAULT_NOUS_PORTAL_URL.rstrip('/')}/login" in msg +def test_poll_for_token_timeout_raises_actionable_message(): + """The poll deadline must raise the CAPTCHA-aware guidance at the SOURCE, + so both the CLI login and the dashboard poller (web_server._nous_poller, + which surfaces str(e) to the UI) inherit it.""" + import httpx + import pytest + + import hermes_cli.auth as auth_mod + + class _PendingClient: + def post(self, url, data=None): + request = httpx.Request("POST", url) + return httpx.Response( + 400, + json={"error": "authorization_pending"}, + request=request, + ) + + from typing import cast + + with pytest.raises(TimeoutError) as excinfo: + auth_mod._poll_for_token( + client=cast(httpx.Client, _PendingClient()), + portal_base_url="https://portal.nousresearch.com", + client_id="hermes-cli", + device_code="device", + expires_in=1, + poll_interval=1, + ) + + msg = str(excinfo.value) + assert "CAPTCHA" in msg + assert "hermes portal" in msg + assert "https://portal.nousresearch.com/login" in msg + + def test_nous_device_code_login_timeout_raises_actionable_message(monkeypatch): - """Poll timeout must surface the CAPTCHA-aware guidance, not a bare line.""" + """Poll timeout must surface the CAPTCHA-aware guidance through the CLI + login flow (propagates unchanged from _poll_for_token).""" import pytest import hermes_cli.auth as auth_mod @@ -1115,7 +1152,11 @@ def test_nous_device_code_login_timeout_raises_actionable_message(monkeypatch): ) def _timeout(**kwargs): - raise TimeoutError("Timed out waiting for device authorization") + raise TimeoutError( + auth_mod._nous_device_auth_timeout_message( + kwargs.get("portal_base_url", "") + ) + ) monkeypatch.setattr(auth_mod, "_poll_for_token", _timeout) monkeypatch.setattr(auth_mod.webbrowser, "open", lambda url: True)