Files
hermes-agent/tests/agent/test_nous_oauth_401_guidance.py
kshitijk4poor da4f407e51 feat(cli): make hermes portal the human-readable Portal onboarding alias
`hermes portal` (no subcommand) now runs the one-shot Nous Portal onboarding
— OAuth login, switch provider to Nous, offer Tool Gateway — identical to
`hermes setup --portal` and the human-readable alias for
`hermes auth add nous --type oauth` (which still works).

The prior status default moves to `hermes portal info`; `status` is kept as a
hidden back-compat alias. `open`/`tools` subcommands are unchanged.

User-facing hints and docs (status.py, conversation_loop 401 guidance,
SystemPage, README, website docs + zh-Hans) now point at `hermes portal` /
`hermes portal info`. `--manual-paste` references keep the explicit auth
command since `hermes portal` does not expose that flag.
2026-06-04 01:19:28 +05:30

72 lines
2.9 KiB
Python

"""Tests for the Nous OAuth 401 actionable-guidance branch in
``agent.conversation_loop.run_conversation``.
Source-inspection style (matches ``test_gemini_fast_fallback.py``): we assert
that the guidance strings exist in the function body so that the user-facing
hint cannot be silently removed by a future refactor.
Regression context: ashh hit a Nous 401 (OAuth token expired / portal said
account out of credits) plus a model slug ``deepseek/deepseek-v4-flash:free``
that's OpenRouter syntax, not a Nous catalog name. The previous guidance
branch only covered ``openai-codex`` and ``xai-oauth``; ``nous`` fell through
to a generic "Your API key was rejected... run hermes setup" message, which is
the wrong advice for a pure-OAuth provider.
"""
from __future__ import annotations
import inspect
from agent import conversation_loop
def test_nous_provider_is_in_oauth_401_set():
"""The provider-set gate that selects OAuth-specific guidance must
include ``nous`` alongside ``openai-codex`` and ``xai-oauth``.
"""
source = inspect.getsource(conversation_loop.run_conversation)
# Be flexible about set element ordering — assert all three are listed
# near each other in the gating expression.
assert "\"openai-codex\"" in source
assert "\"xai-oauth\"" in source
assert "\"nous\"" in source
# And the gate string itself must mention all three so future refactors
# that split nous off into its own gate still get caught.
needle = "_provider in {\"openai-codex\", \"xai-oauth\", \"nous\"}"
assert needle in source, (
"Expected nous to be co-gated with the other OAuth providers in the "
"actionable-401-guidance branch of run_conversation."
)
def test_nous_401_guidance_strings_present():
"""User-facing remediation strings for Nous OAuth 401s must exist."""
source = inspect.getsource(conversation_loop.run_conversation)
# Must tell the user it's an OAuth token problem, NOT an API key problem
# (Nous Portal has no API key path — auth_type=oauth_device_code only).
assert "Nous Portal OAuth token was rejected" in source
# Must give a concrete re-auth command, not a generic "hermes setup".
assert "hermes portal" in source
# Must point at the portal so users can check account/credit status.
assert "portal.nousresearch.com" in source
def test_free_slug_hint_for_nous_provider():
"""When the failing model slug ends with ``:free`` and the provider is
``nous``, the guidance must flag that ``:free`` is OpenRouter syntax and
suggest switching providers via ``/model openrouter:<slug>``.
Without this hint, users re-OAuth successfully and then hit the same 401
on the next message because Nous Portal doesn't carry the OpenRouter
free-tier slug.
"""
source = inspect.getsource(conversation_loop.run_conversation)
assert "endswith(\":free\")" in source
assert "OpenRouter slug" in source
assert "/model openrouter:" in source