hermes-agent/tests/hermes_cli/test_status.py
Teknium febc4cfec0
remove Vercel AI Gateway and Vercel Sandbox (#33067)
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend

Both Vercel-hosted integrations are removed end-to-end. Users on the AI
Gateway should switch to OpenRouter or one of the other aggregators
(Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should
switch to Docker, Modal, Daytona, or SSH.

What's removed:
- `plugins/model-providers/ai-gateway/` provider plugin
- `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper
- `tools/environments/vercel_sandbox.py` terminal backend
- `ai-gateway` provider wiring across auth, doctor, setup, models,
  config, status, providers, main, web_server, model_normalize, dump
- `vercel_sandbox` backend wiring across terminal_tool, file_tools,
  code_execution_tool, file_operations, approval, skills_tool,
  environments/local, credential_files, lazy_deps, prompt_builder,
  cli, gateway/run
- `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client
  header set, run_agent base-URL header/reasoning special-cases
- `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock
- env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`,
  `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`,
  `TERMINAL_VERCEL_RUNTIME`
- Tests: deletes test_ai_gateway_models.py and
  test_vercel_sandbox_environment.py; scrubs references across 23
  surviving test files (no entire tests deleted unless they were
  dedicated to AI Gateway / Sandbox)
- Docs: provider tables, env-var reference, setup guides, security
  notes, tool config, terminal-backend tables — English plus zh-Hans
  i18n parity
- `hermes-agent` skill: provider table entry and remote-backend list

What stays (intentional):
- `popular-web-designs/templates/vercel.md` — CSS design reference,
  unrelated to Vercel-the-AI-product
- `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN
  response header, useful diag signal on any Vercel-hosted endpoint
- `vercel-labs/agent-browser` URL in browser config — lightpanda
  browser project, different OSS effort
- `userStories.json` historical contributor entry mentioning Vercel
  Sandbox — archive, not active docs

Validation:
- 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`)
- Full repo `py_compile` clean
- Live import of every touched module + invariant check (no
  `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no
  `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`)

* test: convert profile-count check from change-detector to invariant

The hardcoded "== 34" assertion broke when ai-gateway was removed.
Per AGENTS.md change-detector-test guidance, assert the relationship
(registry count >= number of plugin dirs) instead of a literal count.
Counts shift when providers are added/removed; that's expected.
2026-05-27 00:43:32 -07:00

303 lines
14 KiB
Python

from types import SimpleNamespace
from hermes_cli.status import show_status
def test_show_status_includes_tavily_key(monkeypatch, capsys, tmp_path):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("TAVILY_API_KEY", "tvly-1234567890abcdef")
show_status(SimpleNamespace(all=False, deep=False))
output = capsys.readouterr().out
assert "Tavily" in output
assert "tvly...cdef" in output
def test_show_status_termux_gateway_section_skips_systemctl(monkeypatch, capsys, tmp_path):
from hermes_cli import status as status_mod
import hermes_cli.auth as auth_mod
import hermes_cli.gateway as gateway_mod
monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)
monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
def _unexpected_systemctl(*args, **kwargs):
raise AssertionError("systemctl should not be called in the Termux status view")
monkeypatch.setattr(status_mod.subprocess, "run", _unexpected_systemctl)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
output = capsys.readouterr().out
assert "Manager: Termux / manual process" in output
assert "Start with: hermes gateway" in output
assert "systemd (user)" not in output
def test_show_status_reports_nous_auth_error(monkeypatch, capsys, tmp_path):
from hermes_cli import status as status_mod
import hermes_cli.auth as auth_mod
import hermes_cli.gateway as gateway_mod
monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)
monkeypatch.setattr(
auth_mod,
"get_nous_auth_status",
lambda: {
"logged_in": False,
"portal_base_url": "https://portal.nousresearch.com",
"access_expires_at": "2026-04-20T01:00:51+00:00",
"agent_key_expires_at": "2026-04-20T04:54:24+00:00",
"has_refresh_token": True,
"error": "Refresh session has been revoked",
},
raising=False,
)
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_qwen_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
output = capsys.readouterr().out
assert "Nous Portal ✗ not logged in (run: hermes auth add nous --type oauth)" in output
assert "Error: Refresh session has been revoked" in output
assert "Access exp:" in output
assert "Key exp:" in output
# ---------------------------------------------------------------------------
# Helpers shared by xAI OAuth status tests
# ---------------------------------------------------------------------------
def _base_xai_mocks(monkeypatch, tmp_path):
"""Set up the minimal environment for show_status, returning status_mod."""
from hermes_cli import status as status_mod
import hermes_cli.auth as auth_mod
import hermes_cli.gateway as gateway_mod
monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)
monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_qwen_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_minimax_oauth_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
return status_mod
class TestShowStatusXaiOAuth:
"""xAI OAuth row in hermes status."""
# ------------------------------------------------------------------
# Logged-in branch
# ------------------------------------------------------------------
def test_logged_in_shows_check_mark_and_label(self, monkeypatch, capsys, tmp_path):
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": True, "auth_store": "/a/auth.json"},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "xAI OAuth" in out
# The logged-in label must appear; the "not logged in" label must not
assert "" in out or "logged in" in out
assert "not logged in" not in out.split("xAI OAuth", 1)[1].split("\n")[0]
def test_logged_in_shows_auth_store(self, monkeypatch, capsys, tmp_path):
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": True, "auth_store": "/home/u/.hermes/auth.json"},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "Auth file: /home/u/.hermes/auth.json" in out
def test_logged_in_shows_last_refresh(self, monkeypatch, capsys, tmp_path):
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {
"logged_in": True,
"auth_store": "/a/auth.json",
"last_refresh": "2026-05-17T10:00:00+00:00",
},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "Refreshed:" in out
def test_logged_in_does_not_show_error_line(self, monkeypatch, capsys, tmp_path):
"""Error field must be suppressed when logged_in is True."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {
"logged_in": True,
"auth_store": "/a/auth.json",
"error": "stale-error-must-not-appear",
},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
xai_section = out.split("xAI OAuth", 1)[1]
assert "stale-error-must-not-appear" not in xai_section
def test_no_auth_store_line_when_field_absent(self, monkeypatch, capsys, tmp_path):
"""Auth file line must not appear when auth_store is missing."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": True},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
xai_section = out.split("xAI OAuth", 1)[1].split("", 1)[0]
assert "Auth file:" not in xai_section
def test_no_refreshed_line_when_last_refresh_absent(self, monkeypatch, capsys, tmp_path):
"""Refreshed line must not appear when last_refresh is not present."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": True, "auth_store": "/a/auth.json"},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
xai_section = out.split("xAI OAuth", 1)[1].split("", 1)[0]
assert "Refreshed:" not in xai_section
# ------------------------------------------------------------------
# Not-logged-in branch
# ------------------------------------------------------------------
def test_not_logged_in_shows_login_command(self, monkeypatch, capsys, tmp_path):
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": False, "error": "no credentials"},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "not logged in (run: hermes auth add xai-oauth)" in out
def test_not_logged_in_shows_error(self, monkeypatch, capsys, tmp_path):
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": False, "error": "Token has expired"},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "Error: Token has expired" in out
def test_not_logged_in_omits_error_line_when_error_absent(self, monkeypatch, capsys, tmp_path):
"""No Error: line when not logged in but error key is missing."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: {"logged_in": False},
raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
xai_section = out.split("xAI OAuth", 1)[1].split("", 1)[0]
assert "Error:" not in xai_section
# ------------------------------------------------------------------
# Resilience: import failure and runtime exception
# ------------------------------------------------------------------
def test_import_failure_does_not_crash_show_status(self, monkeypatch, capsys, tmp_path):
"""show_status must complete even when get_xai_oauth_auth_status cannot be imported."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.delattr(auth_mod, "get_xai_oauth_auth_status", raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "◆ Auth Providers" in out
def test_import_failure_does_not_break_other_oauth_providers(self, monkeypatch, capsys, tmp_path):
"""Nous/Codex/MiniMax rows must still appear when xAI import fails."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_nous_auth_status",
lambda: {"logged_in": True}, raising=False)
monkeypatch.delattr(auth_mod, "get_xai_oauth_auth_status", raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "Nous Portal" in out
assert "MiniMax OAuth" in out
def test_status_function_exception_does_not_crash(self, monkeypatch, capsys, tmp_path):
"""show_status must not propagate an exception raised by get_xai_oauth_auth_status."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
def _raises():
raise RuntimeError("backend unreachable")
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status", _raises, raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "◆ Auth Providers" in out
def test_status_function_returns_none_does_not_crash(self, monkeypatch, capsys, tmp_path):
"""get_xai_oauth_auth_status returning None must be handled gracefully."""
import hermes_cli.auth as auth_mod
status_mod = _base_xai_mocks(monkeypatch, tmp_path)
monkeypatch.setattr(auth_mod, "get_xai_oauth_auth_status",
lambda: None, raising=False)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
out = capsys.readouterr().out
assert "xAI OAuth" in out
assert "not logged in (run: hermes auth add xai-oauth)" in out