fix(xai): accept Grok Build code during loopback wait + tiny screenshot guard
xAI's consent page renders the authorization code in-page instead of redirecting to the loopback callback, so the listener just hangs and the manual-paste flow demands a callback URL that never contains the token. - auth.py: poll stdin non-blockingly while waiting for the xAI loopback callback; accept a pasted bare Grok Build code and substitute the locally generated state (PKCE code_verifier still binds the exchange). No need to wait for timeout or re-run with --manual-paste. - computer_use: parse PNG/JPEG dimensions from base64 and fall back to the text/AX/SOM payload when the screenshot is below the provider minimum (8x8), which xAI rejects with HTTP 400. - model_setup_flows.py: xAI credential reuse prompt uses the standard radio picker via a shared _prompt_auth_credentials_choice helper. - main.py: thread a title through _prompt_provider_choice; re-home the helper import (flows live in model_setup_flows.py post-decomposition). Salvaged from #36781 onto current main (contributor's main.py edits re-homed to model_setup_flows.py, where the flows were extracted since the PR opened).
This commit is contained in:
@@ -465,7 +465,7 @@ def test_xai_loopback_login_manual_paste_missing_code_raises(monkeypatch):
|
||||
|
||||
|
||||
def test_xai_loopback_login_timeout_falls_back_to_manual_paste(monkeypatch):
|
||||
"""Loopback timeout should offer the existing manual-paste path."""
|
||||
"""Loopback timeout should accept a bare Grok Build code paste."""
|
||||
monkeypatch.setattr(
|
||||
auth_mod, "_xai_oauth_discovery",
|
||||
lambda *_a, **_k: {
|
||||
@@ -523,7 +523,7 @@ def test_xai_loopback_login_timeout_falls_back_to_manual_paste(monkeypatch):
|
||||
captured["prompt_calls"] += 1
|
||||
return {
|
||||
"code": "manual-auth-code",
|
||||
"state": captured["state"],
|
||||
"state": None,
|
||||
"error": None,
|
||||
"error_description": None,
|
||||
}
|
||||
@@ -558,6 +558,48 @@ def test_xai_loopback_login_timeout_falls_back_to_manual_paste(monkeypatch):
|
||||
assert creds["tokens"]["refresh_token"] == "rt-timeout"
|
||||
|
||||
|
||||
def test_xai_wait_for_callback_accepts_ready_stdin_code(monkeypatch):
|
||||
"""Users can paste the Grok Build code while Hermes is still waiting."""
|
||||
class _StubServer:
|
||||
shutdown_called = False
|
||||
close_called = False
|
||||
|
||||
def shutdown(self):
|
||||
self.shutdown_called = True
|
||||
|
||||
def server_close(self):
|
||||
self.close_called = True
|
||||
|
||||
class _StubThread:
|
||||
joined = False
|
||||
|
||||
def join(self, timeout=None):
|
||||
self.joined = True
|
||||
|
||||
server = _StubServer()
|
||||
thread = _StubThread()
|
||||
monkeypatch.setattr(
|
||||
auth_mod,
|
||||
"_read_ready_stdin_line",
|
||||
lambda: "ready-grok-build-code\n",
|
||||
)
|
||||
|
||||
out = auth_mod._xai_wait_for_callback(
|
||||
server,
|
||||
thread,
|
||||
{"code": None, "error": None},
|
||||
timeout_seconds=5,
|
||||
manual_paste_redirect_uri="http://127.0.0.1:56121/callback",
|
||||
)
|
||||
|
||||
assert out["code"] == "ready-grok-build-code"
|
||||
assert out["state"] is None
|
||||
assert out["_manual_paste"] is True
|
||||
assert server.shutdown_called is True
|
||||
assert server.close_called is True
|
||||
assert thread.joined is True
|
||||
|
||||
|
||||
def test_xai_loopback_login_timeout_noninteractive_reraises(monkeypatch):
|
||||
"""Non-interactive stdin must keep the original timeout error."""
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import argparse
|
||||
|
||||
|
||||
def test_xai_model_flow_reauth_uses_standard_radio_prompt(monkeypatch):
|
||||
from hermes_cli import main as main_mod
|
||||
|
||||
captured = {"login_calls": 0}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_xai_oauth_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.setup._curses_prompt_choice",
|
||||
lambda title, choices, default, description=None: 1,
|
||||
)
|
||||
|
||||
def _fake_login(args, provider, force_new_login=False):
|
||||
captured["login_calls"] += 1
|
||||
captured["force_new_login"] = force_new_login
|
||||
captured["args"] = args
|
||||
|
||||
monkeypatch.setattr("hermes_cli.auth._login_xai_oauth", _fake_login)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.resolve_xai_oauth_runtime_credentials",
|
||||
lambda *args, **kwargs: {"base_url": "https://api.x.ai/v1"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._prompt_model_selection",
|
||||
lambda model_ids, current_model="": None,
|
||||
)
|
||||
|
||||
main_mod._model_flow_xai_oauth(
|
||||
{},
|
||||
current_model="grok-build-0.1",
|
||||
args=argparse.Namespace(manual_paste=True, no_browser=True, timeout=3),
|
||||
)
|
||||
|
||||
assert captured["login_calls"] == 1
|
||||
assert captured["force_new_login"] is True
|
||||
assert captured["args"].manual_paste is True
|
||||
assert captured["args"].no_browser is True
|
||||
assert captured["args"].timeout == 3
|
||||
|
||||
|
||||
def test_xai_model_flow_cancel_skips_reauth(monkeypatch):
|
||||
from hermes_cli import main as main_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_xai_oauth_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.setup._curses_prompt_choice",
|
||||
lambda title, choices, default, description=None: 2,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._login_xai_oauth",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not reauthenticate")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._prompt_model_selection",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not pick a model")),
|
||||
)
|
||||
|
||||
main_mod._model_flow_xai_oauth({}, current_model="grok-build-0.1")
|
||||
|
||||
|
||||
def test_auth_credentials_choice_falls_back_to_numbered_prompt(monkeypatch):
|
||||
from hermes_cli import main as main_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.setup._curses_prompt_choice",
|
||||
lambda title, choices, default, description=None: -1,
|
||||
)
|
||||
monkeypatch.setattr("builtins.input", lambda prompt="": "2")
|
||||
|
||||
assert main_mod._prompt_auth_credentials_choice("Credentials:") == "reauth"
|
||||
Reference in New Issue
Block a user