fix(cli): auth.json-only login with a benched credential is explained, not sent to the wizard
Two gaps from review of #113720's fix: 1. A profile logged in via auth.json (active_provider: nous) with no model.provider in config.yaml resolves as "auto". The ladder's OAuth rung swallowed the AuthError for "auto" and fell through to the keyless OpenRouter fallback, so the startup probe returned (False, None) and the first-run wizard ran anyway. The ladder now catches the AuthError in _ladder_rungs, still falls through for "auto", but stamps the swallowed error on a keyless fallback as `auth_error`; _probe_runtime_credentials returns it so the notice names the real failure. 2. The gate itself lived only in cli.py::_tui_print_startup and was untested at the seam (reverting cli.py left the suite green). It is now one mixin method, _maybe_offer_first_run_setup (tty check → probe → explain → offer), called from _tui_print_startup, and both tests drive that method with stdin.isatty patched True and _offer_first_run_setup asserting it is not called. Tests: the benched-credential test now covers the gate and the cooldown headline wording; the blank-install control is folded into the new auth.json-only test.
This commit is contained in:
@@ -394,8 +394,9 @@ class CLIAgentSetupMixin:
|
||||
return self._probe_runtime_credentials()[0]
|
||||
|
||||
def _probe_runtime_credentials(self) -> tuple:
|
||||
"""``(ready, error)``: *error* is the exception that stopped resolution, ``None`` when a
|
||||
provider resolved (usable or merely keyless). Never prints or mutates CLI state."""
|
||||
"""``(ready, error)``: *error* is the exception that stopped resolution — raised, or
|
||||
swallowed by the "auto" ladder and stamped on a keyless fallback — ``None`` when a provider
|
||||
resolved (usable or merely keyless). Never prints or mutates CLI state."""
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
try:
|
||||
runtime = resolve_runtime_provider(
|
||||
@@ -409,7 +410,16 @@ class CLIAgentSetupMixin:
|
||||
base_url = runtime.get("base_url")
|
||||
if callable(api_key) or (isinstance(api_key, str) and api_key):
|
||||
return bool(base_url), None
|
||||
return _keyless_custom_base(base_url), None
|
||||
return _keyless_custom_base(base_url), runtime.get("auth_error")
|
||||
|
||||
def _maybe_offer_first_run_setup(self) -> None:
|
||||
"""Interactive startup gate: a blank install goes to the provider wizard; a configured
|
||||
profile whose credential is benched or signed out gets the reason instead (#113720)."""
|
||||
if not sys.stdin.isatty():
|
||||
return
|
||||
ready, error = self._probe_runtime_credentials()
|
||||
if not ready and not self._explain_unusable_credentials(error):
|
||||
self._offer_first_run_setup()
|
||||
|
||||
def _explain_unusable_credentials(self, error) -> bool:
|
||||
"""A configured profile whose credential is benched, quarantined or signed out is not a
|
||||
|
||||
Reference in New Issue
Block a user