fix(auth): enrich device-auth timeout at the source to cover the dashboard poller

/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.
This commit is contained in:
kshitijk4poor 2026-08-02 13:22:55 +05:30 committed by kshitij
parent fbf26e3845
commit 840fb55a8a
2 changed files with 55 additions and 16 deletions

View File

@ -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))

View File

@ -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)