hermes-agent/tests/gateway/test_config_cwd_bridge.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

246 lines
10 KiB
Python

"""Tests for the config.yaml → env var bridge logic in gateway/run.py.
Specifically tests that top-level `cwd:` and `backend:` in config.yaml
are correctly bridged to TERMINAL_CWD / TERMINAL_ENV env vars as
convenience aliases for `terminal.cwd` / `terminal.backend`.
The bridge logic is module-level code in gateway/run.py, so we test
the semantics by reimplementing the relevant config bridge snippet and
asserting the expected env var outcomes.
"""
import os
import json
import pytest
def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
"""Simulate the gateway config bridge logic from gateway/run.py.
Returns the resulting env dict (only TERMINAL_* and MESSAGING_CWD keys).
"""
env = dict(initial_env or {})
# --- Replicate lines 54-56: generic top-level bridge (for context) ---
for key, val in cfg.items():
if isinstance(val, (str, int, float, bool)) and key not in env:
env[key] = str(val)
# --- Replicate lines 59-87: terminal config bridge ---
terminal_cfg = cfg.get("terminal", {})
if terminal_cfg and isinstance(terminal_cfg, dict):
terminal_env_map = {
"backend": "TERMINAL_ENV",
"cwd": "TERMINAL_CWD",
"timeout": "TERMINAL_TIMEOUT",
"container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
"container_cpu": "TERMINAL_CONTAINER_CPU",
"container_memory": "TERMINAL_CONTAINER_MEMORY",
"container_disk": "TERMINAL_CONTAINER_DISK",
}
for cfg_key, env_var in terminal_env_map.items():
if cfg_key in terminal_cfg:
val = terminal_cfg[cfg_key]
# Skip cwd placeholder values — don't overwrite already-resolved
# TERMINAL_CWD. Mirrors the fix in gateway/run.py.
if cfg_key == "cwd" and str(val) in {".", "auto", "cwd"}:
continue
# Expand shell tilde so subprocess.Popen never receives a literal
# "~/" which the kernel rejects.
if cfg_key == "cwd" and isinstance(val, str):
val = os.path.expanduser(val)
if isinstance(val, list):
env[env_var] = json.dumps(val)
else:
env[env_var] = str(val)
# --- NEW: top-level aliases (the fix being tested) ---
top_level_aliases = {
"cwd": "TERMINAL_CWD",
"backend": "TERMINAL_ENV",
}
for alias_key, alias_env in top_level_aliases.items():
if alias_env not in env:
alias_val = cfg.get(alias_key)
if isinstance(alias_val, str) and alias_val.strip():
if alias_key == "cwd":
alias_val = os.path.expanduser(alias_val)
env[alias_env] = alias_val.strip()
# --- Replicate lines 144-147: MESSAGING_CWD fallback ---
configured_cwd = env.get("TERMINAL_CWD", "")
if not configured_cwd or configured_cwd in {".", "auto", "cwd"}:
messaging_cwd = env.get("MESSAGING_CWD") or "/root" # Path.home() for root
env["TERMINAL_CWD"] = messaging_cwd
return env
class TestTopLevelCwdAlias:
"""Top-level `cwd:` should be treated as `terminal.cwd`."""
def test_top_level_cwd_sets_terminal_cwd(self):
cfg = {"cwd": "/home/hermes/projects"}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
def test_top_level_backend_sets_terminal_env(self):
cfg = {"backend": "docker"}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_ENV"] == "docker"
def test_top_level_cwd_and_backend(self):
cfg = {"backend": "local", "cwd": "/home/hermes/projects"}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
assert result["TERMINAL_ENV"] == "local"
def test_nested_terminal_takes_precedence_over_top_level(self):
"""terminal.cwd should win over top-level cwd."""
cfg = {
"cwd": "/should/not/use",
"terminal": {"cwd": "/home/hermes/real"},
}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == "/home/hermes/real"
def test_nested_terminal_backend_takes_precedence(self):
cfg = {
"backend": "should-not-use",
"terminal": {"backend": "docker"},
}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_ENV"] == "docker"
def test_no_cwd_falls_back_to_messaging_cwd(self):
cfg = {}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes/projects"})
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
def test_no_cwd_no_messaging_cwd_falls_back_to_home(self):
cfg = {}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == "/root" # Path.home() for root user
def test_dot_cwd_triggers_messaging_fallback(self):
"""cwd: '.' should trigger MESSAGING_CWD fallback."""
cfg = {"cwd": "."}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
# "." is stripped but truthy, so it gets set as TERMINAL_CWD
# Then the MESSAGING_CWD fallback does NOT trigger since TERMINAL_CWD
# is set and not in (".", "auto", "cwd").
# Wait — "." IS in the fallback list! So this should fall through.
# Actually the alias sets it to ".", then the messaging fallback
# checks if it's in (".", "auto", "cwd") and overrides.
assert result["TERMINAL_CWD"] == "/home/hermes"
def test_auto_cwd_triggers_messaging_fallback(self):
cfg = {"cwd": "auto"}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
assert result["TERMINAL_CWD"] == "/home/hermes"
def test_empty_cwd_ignored(self):
cfg = {"cwd": ""}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
assert result["TERMINAL_CWD"] == "/home/hermes"
def test_whitespace_only_cwd_ignored(self):
cfg = {"cwd": " "}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/fallback"})
assert result["TERMINAL_CWD"] == "/fallback"
def test_messaging_cwd_env_var_works(self):
"""MESSAGING_CWD in initial env should be picked up as fallback."""
cfg = {}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes/projects"})
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
def test_top_level_cwd_beats_messaging_cwd(self):
"""Explicit top-level cwd should take precedence over MESSAGING_CWD."""
cfg = {"cwd": "/from/config"}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
assert result["TERMINAL_CWD"] == "/from/config"
class TestNestedTerminalCwdPlaceholderSkip:
"""terminal.cwd placeholder values must not clobber TERMINAL_CWD.
When config.yaml has terminal.cwd: "." (or "auto"/"cwd"), the gateway
config bridge should NOT write that placeholder to TERMINAL_CWD.
This prevents .env or MESSAGING_CWD values from being overwritten.
See issues #10225, #4672, #10817.
"""
def test_terminal_dot_cwd_does_not_clobber_env(self):
"""terminal.cwd: '.' should not overwrite a pre-set TERMINAL_CWD."""
cfg = {"terminal": {"cwd": "."}}
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
assert result["TERMINAL_CWD"] == "/my/project"
def test_terminal_auto_cwd_does_not_clobber_env(self):
cfg = {"terminal": {"cwd": "auto"}}
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
assert result["TERMINAL_CWD"] == "/my/project"
def test_terminal_cwd_keyword_does_not_clobber_env(self):
cfg = {"terminal": {"cwd": "cwd"}}
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
assert result["TERMINAL_CWD"] == "/my/project"
def test_terminal_explicit_cwd_does_override(self):
"""terminal.cwd: '/explicit/path' SHOULD override TERMINAL_CWD."""
cfg = {"terminal": {"cwd": "/explicit/path"}}
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/old/value"})
assert result["TERMINAL_CWD"] == "/explicit/path"
def test_terminal_dot_cwd_falls_back_to_messaging_cwd(self):
"""terminal.cwd: '.' with no TERMINAL_CWD should fall to MESSAGING_CWD."""
cfg = {"terminal": {"cwd": "."}}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
assert result["TERMINAL_CWD"] == "/from/env"
def test_terminal_dot_cwd_and_messaging_cwd_both_set(self):
"""Pre-set TERMINAL_CWD from .env wins over terminal.cwd: '.'."""
cfg = {"terminal": {"cwd": ".", "backend": "local"}}
result = _simulate_config_bridge(cfg, {
"TERMINAL_CWD": "/my/project",
"MESSAGING_CWD": "/fallback",
})
assert result["TERMINAL_CWD"] == "/my/project"
def test_non_cwd_terminal_keys_still_bridge(self):
"""Other terminal config keys (backend, timeout) should still bridge normally."""
cfg = {"terminal": {"cwd": ".", "backend": "docker", "timeout": "300"}}
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
assert result["TERMINAL_ENV"] == "docker"
assert result["TERMINAL_TIMEOUT"] == "300"
assert result["TERMINAL_CWD"] == "/from/env"
class TestTildeExpansion:
"""terminal.cwd values containing shell tilde must be expanded.
subprocess.Popen does not expand shell syntax, so a literal "~/"
causes FileNotFoundError. Regression test for commit 3c42064e.
"""
def test_terminal_cwd_tilde_expanded(self):
"""terminal.cwd: '~/projects' should expand to /home/<user>/projects."""
cfg = {"terminal": {"cwd": "~/projects"}}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == os.path.expanduser("~/projects")
def test_top_level_cwd_tilde_expanded(self):
"""top-level cwd: '~/' should expand to user's home directory."""
cfg = {"cwd": "~/"}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == os.path.expanduser("~/")
def test_tilde_with_nested_precedence(self):
"""Nested terminal.cwd should win over top-level, both expanded."""
cfg = {
"cwd": "~/top",
"terminal": {"cwd": "~/nested"},
}
result = _simulate_config_bridge(cfg)
assert result["TERMINAL_CWD"] == os.path.expanduser("~/nested")