mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
* 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.
303 lines
14 KiB
Python
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
|