mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
579 lines
26 KiB
Python
579 lines
26 KiB
Python
"""Tests for web backend client configuration and singleton behavior.
|
|
|
|
Coverage:
|
|
_get_firecrawl_client() — configuration matrix, singleton caching,
|
|
constructor failure recovery, return value verification, edge cases.
|
|
_get_backend() — backend selection logic with env var combinations.
|
|
_get_parallel_client() — Parallel client configuration, singleton caching.
|
|
check_web_api_key() — unified availability check across all web backends.
|
|
"""
|
|
|
|
import importlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import types
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
|
|
|
|
class TestFirecrawlClientConfig:
|
|
"""Test suite for Firecrawl client initialization."""
|
|
|
|
def setup_method(self):
|
|
"""Reset client and env vars before each test."""
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
web_tools_mod._firecrawl_client = None
|
|
web_tools_mod._firecrawl_client_config = None
|
|
for key in (
|
|
"FIRECRAWL_API_KEY",
|
|
"FIRECRAWL_API_URL",
|
|
"FIRECRAWL_GATEWAY_URL",
|
|
"TOOL_GATEWAY_DOMAIN",
|
|
"TOOL_GATEWAY_SCHEME",
|
|
"TOOL_GATEWAY_USER_TOKEN",
|
|
):
|
|
os.environ.pop(key, None)
|
|
# Enable managed tools by default for these tests — patch both the
|
|
# local web_tools import and the managed_tool_gateway import so the
|
|
# full firecrawl client init path sees True.
|
|
self._managed_patchers = [
|
|
patch("hermes_agent.tools.web.managed_nous_tools_enabled", return_value=True),
|
|
patch("hermes_agent.tools.managed_gateway.managed_nous_tools_enabled", return_value=True),
|
|
]
|
|
for p in self._managed_patchers:
|
|
p.start()
|
|
|
|
def teardown_method(self):
|
|
"""Reset client after each test."""
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
web_tools_mod._firecrawl_client = None
|
|
web_tools_mod._firecrawl_client_config = None
|
|
for key in (
|
|
"FIRECRAWL_API_KEY",
|
|
"FIRECRAWL_API_URL",
|
|
"FIRECRAWL_GATEWAY_URL",
|
|
"TOOL_GATEWAY_DOMAIN",
|
|
"TOOL_GATEWAY_SCHEME",
|
|
"TOOL_GATEWAY_USER_TOKEN",
|
|
):
|
|
os.environ.pop(key, None)
|
|
for p in self._managed_patchers:
|
|
p.stop()
|
|
|
|
# ── Configuration matrix ─────────────────────────────────────────
|
|
|
|
def test_no_config_raises_with_helpful_message(self):
|
|
"""Neither key nor URL → ValueError with guidance."""
|
|
with patch("hermes_agent.tools.web.Firecrawl"):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value=None):
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
with pytest.raises(ValueError, match="FIRECRAWL_API_KEY"):
|
|
_get_firecrawl_client()
|
|
|
|
def test_tool_gateway_domain_builds_firecrawl_gateway_origin(self):
|
|
"""Shared gateway domain should derive the Firecrawl vendor hostname."""
|
|
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
result = _get_firecrawl_client()
|
|
mock_fc.assert_called_once_with(
|
|
api_key="nous-token",
|
|
api_url="https://firecrawl-gateway.nousresearch.com",
|
|
)
|
|
assert result is mock_fc.return_value
|
|
|
|
def test_tool_gateway_scheme_can_switch_derived_gateway_origin_to_http(self):
|
|
"""Shared gateway scheme should allow local plain-http vendor hosts."""
|
|
with patch.dict(os.environ, {
|
|
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
|
"TOOL_GATEWAY_SCHEME": "http",
|
|
}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
result = _get_firecrawl_client()
|
|
mock_fc.assert_called_once_with(
|
|
api_key="nous-token",
|
|
api_url="http://firecrawl-gateway.nousresearch.com",
|
|
)
|
|
assert result is mock_fc.return_value
|
|
|
|
def test_invalid_tool_gateway_scheme_raises(self):
|
|
"""Unexpected shared gateway schemes should fail fast."""
|
|
with patch.dict(os.environ, {
|
|
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
|
"TOOL_GATEWAY_SCHEME": "ftp",
|
|
}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
with pytest.raises(ValueError, match="TOOL_GATEWAY_SCHEME"):
|
|
_get_firecrawl_client()
|
|
|
|
def test_explicit_firecrawl_gateway_url_takes_precedence(self):
|
|
"""An explicit Firecrawl gateway origin should override the shared domain."""
|
|
with patch.dict(os.environ, {
|
|
"FIRECRAWL_GATEWAY_URL": "https://firecrawl-gateway.localhost:3009/",
|
|
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
|
}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
_get_firecrawl_client()
|
|
mock_fc.assert_called_once_with(
|
|
api_key="nous-token",
|
|
api_url="https://firecrawl-gateway.localhost:3009",
|
|
)
|
|
|
|
def test_default_gateway_domain_targets_nous_production_origin(self):
|
|
"""Default gateway origin should point at the Firecrawl vendor hostname."""
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
_get_firecrawl_client()
|
|
mock_fc.assert_called_once_with(
|
|
api_key="nous-token",
|
|
api_url="https://firecrawl-gateway.nousresearch.com",
|
|
)
|
|
|
|
def test_nous_auth_token_respects_hermes_home_override(self, tmp_path):
|
|
"""Auth lookup should read from HERMES_HOME/auth.json, not ~/.hermes/auth.json."""
|
|
real_home = tmp_path / "real-home"
|
|
(real_home / ".hermes").mkdir(parents=True)
|
|
|
|
hermes_home = tmp_path / "hermes-home"
|
|
hermes_home.mkdir()
|
|
(hermes_home / "auth.json").write_text(json.dumps({
|
|
"providers": {
|
|
"nous": {
|
|
"access_token": "nous-token",
|
|
}
|
|
}
|
|
}))
|
|
|
|
with patch.dict(os.environ, {
|
|
"HOME": str(real_home),
|
|
"HERMES_HOME": str(hermes_home),
|
|
}, clear=False):
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
importlib.reload(web_tools_mod)
|
|
assert web_tools_mod._read_nous_access_token() == "nous-token"
|
|
|
|
def test_check_auxiliary_model_re_resolves_backend_each_call(self):
|
|
"""Availability checks should not be pinned to module import state."""
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
|
|
# Simulate the pre-fix import-time cache slot for regression coverage.
|
|
web_tools_mod.__dict__["_aux_async_client"] = None
|
|
|
|
with patch(
|
|
"hermes_agent.tools.web.get_async_text_auxiliary_client",
|
|
side_effect=[(None, None), (MagicMock(base_url="https://api.openrouter.ai/v1"), "test-model")],
|
|
):
|
|
assert web_tools_mod.check_auxiliary_model() is False
|
|
assert web_tools_mod.check_auxiliary_model() is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_summarizer_re_resolves_backend_after_initial_unavailable_state(self):
|
|
"""Summarization should pick up a backend that becomes available later in-process."""
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
|
|
web_tools_mod.__dict__["_aux_async_client"] = None
|
|
|
|
response = MagicMock()
|
|
response.choices = [MagicMock(message=MagicMock(content="summary text"))]
|
|
|
|
with patch(
|
|
"hermes_agent.tools.web._resolve_web_extract_auxiliary",
|
|
side_effect=[(None, None, {}), (MagicMock(base_url="https://api.openrouter.ai/v1"), "test-model", {})],
|
|
), patch(
|
|
"hermes_agent.tools.web.async_call_llm",
|
|
new=AsyncMock(return_value=response),
|
|
) as mock_async_call:
|
|
assert web_tools_mod.check_auxiliary_model() is False
|
|
result = await web_tools_mod._call_summarizer_llm(
|
|
"Some content worth summarizing",
|
|
"Source: https://example.com\n\n",
|
|
None,
|
|
)
|
|
|
|
assert result == "summary text"
|
|
mock_async_call.assert_awaited_once()
|
|
|
|
# ── Singleton caching ────────────────────────────────────────────
|
|
|
|
def test_singleton_returns_same_instance(self):
|
|
"""Second call returns cached client without re-constructing."""
|
|
with patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
client1 = _get_firecrawl_client()
|
|
client2 = _get_firecrawl_client()
|
|
assert client1 is client2
|
|
mock_fc.assert_called_once() # constructed only once
|
|
|
|
def test_constructor_failure_allows_retry(self):
|
|
"""If Firecrawl() raises, next call should retry (not return None)."""
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
with patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}):
|
|
with patch("hermes_agent.tools.web.Firecrawl") as mock_fc:
|
|
mock_fc.side_effect = [RuntimeError("init failed"), MagicMock()]
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
|
|
with pytest.raises(RuntimeError):
|
|
_get_firecrawl_client()
|
|
|
|
# Client stayed None, so retry should work
|
|
assert web_tools_mod._firecrawl_client is None
|
|
result = _get_firecrawl_client()
|
|
assert result is not None
|
|
|
|
# ── Edge cases ───────────────────────────────────────────────────
|
|
|
|
def test_empty_string_key_no_url_raises(self):
|
|
"""FIRECRAWL_API_KEY='' with no URL → should raise."""
|
|
with patch.dict(os.environ, {"FIRECRAWL_API_KEY": ""}):
|
|
with patch("hermes_agent.tools.web.Firecrawl"):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value=None):
|
|
from hermes_agent.tools.web import _get_firecrawl_client
|
|
with pytest.raises(ValueError):
|
|
_get_firecrawl_client()
|
|
|
|
|
|
class TestBackendSelection:
|
|
"""Test suite for _get_backend() backend selection logic.
|
|
|
|
The backend is configured via config.yaml (web.backend), set by
|
|
``hermes tools``. Falls back to key-based detection for legacy/manual
|
|
setups.
|
|
"""
|
|
|
|
_ENV_KEYS = (
|
|
"EXA_API_KEY",
|
|
"PARALLEL_API_KEY",
|
|
"FIRECRAWL_API_KEY",
|
|
"FIRECRAWL_API_URL",
|
|
"FIRECRAWL_GATEWAY_URL",
|
|
"TOOL_GATEWAY_DOMAIN",
|
|
"TOOL_GATEWAY_SCHEME",
|
|
"TOOL_GATEWAY_USER_TOKEN",
|
|
"TAVILY_API_KEY",
|
|
)
|
|
|
|
def setup_method(self):
|
|
for key in self._ENV_KEYS:
|
|
os.environ.pop(key, None)
|
|
self._managed_patchers = [
|
|
patch("hermes_agent.tools.web.managed_nous_tools_enabled", return_value=True),
|
|
patch("hermes_agent.tools.managed_gateway.managed_nous_tools_enabled", return_value=True),
|
|
]
|
|
for p in self._managed_patchers:
|
|
p.start()
|
|
|
|
def teardown_method(self):
|
|
for key in self._ENV_KEYS:
|
|
os.environ.pop(key, None)
|
|
for p in self._managed_patchers:
|
|
p.stop()
|
|
|
|
# ── Config-based selection (web.backend in config.yaml) ───────────
|
|
|
|
def test_config_parallel(self):
|
|
"""web.backend=parallel in config → 'parallel' regardless of keys."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "parallel"}):
|
|
assert _get_backend() == "parallel"
|
|
|
|
def test_config_exa(self):
|
|
"""web.backend=exa in config → 'exa' regardless of other keys."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "exa"}), \
|
|
patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
assert _get_backend() == "exa"
|
|
|
|
def test_config_firecrawl(self):
|
|
"""web.backend=firecrawl in config → 'firecrawl' even if Parallel key set."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "firecrawl"}), \
|
|
patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
assert _get_backend() == "firecrawl"
|
|
|
|
def test_config_tavily(self):
|
|
"""web.backend=tavily in config → 'tavily' regardless of other keys."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "tavily"}):
|
|
assert _get_backend() == "tavily"
|
|
|
|
def test_config_tavily_overrides_env_keys(self):
|
|
"""web.backend=tavily in config → 'tavily' even if Firecrawl key set."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "tavily"}), \
|
|
patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}):
|
|
assert _get_backend() == "tavily"
|
|
|
|
def test_config_case_insensitive(self):
|
|
"""web.backend=Parallel (mixed case) → 'parallel'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "Parallel"}):
|
|
assert _get_backend() == "parallel"
|
|
|
|
def test_config_tavily_case_insensitive(self):
|
|
"""web.backend=Tavily (mixed case) → 'tavily'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "Tavily"}):
|
|
assert _get_backend() == "tavily"
|
|
|
|
# ── Fallback (no web.backend in config) ───────────────────────────
|
|
|
|
def test_fallback_parallel_only_key(self):
|
|
"""Only PARALLEL_API_KEY set → 'parallel'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
assert _get_backend() == "parallel"
|
|
|
|
def test_fallback_exa_only_key(self):
|
|
"""Only EXA_API_KEY set → 'exa'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"EXA_API_KEY": "exa-test"}):
|
|
assert _get_backend() == "exa"
|
|
|
|
def test_fallback_parallel_takes_priority_over_exa(self):
|
|
"""Exa should only win the fallback path when it is the only configured backend."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"EXA_API_KEY": "exa-test", "PARALLEL_API_KEY": "par-test"}):
|
|
assert _get_backend() == "parallel"
|
|
|
|
def test_fallback_tavily_only_key(self):
|
|
"""Only TAVILY_API_KEY set → 'tavily'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"TAVILY_API_KEY": "tvly-test"}):
|
|
assert _get_backend() == "tavily"
|
|
|
|
def test_fallback_tavily_with_firecrawl_prefers_firecrawl(self):
|
|
"""Tavily + Firecrawl keys, no config → 'firecrawl' (backward compat)."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"TAVILY_API_KEY": "tvly-test", "FIRECRAWL_API_KEY": "fc-test"}):
|
|
assert _get_backend() == "firecrawl"
|
|
|
|
def test_fallback_tavily_with_parallel_prefers_parallel(self):
|
|
"""Tavily + Parallel keys, no config → 'parallel' (Parallel takes priority over Tavily)."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"TAVILY_API_KEY": "tvly-test", "PARALLEL_API_KEY": "par-test"}):
|
|
# Parallel + no Firecrawl → parallel
|
|
assert _get_backend() == "parallel"
|
|
|
|
def test_fallback_both_keys_defaults_to_firecrawl(self):
|
|
"""Both keys set, no config → 'firecrawl' (backward compat)."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key", "FIRECRAWL_API_KEY": "fc-test"}):
|
|
assert _get_backend() == "firecrawl"
|
|
|
|
def test_fallback_firecrawl_only_key(self):
|
|
"""Only FIRECRAWL_API_KEY set → 'firecrawl'."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}), \
|
|
patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}):
|
|
assert _get_backend() == "firecrawl"
|
|
|
|
def test_fallback_no_keys_defaults_to_firecrawl(self):
|
|
"""No keys, no config → 'firecrawl' (will fail at client init)."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={}):
|
|
assert _get_backend() == "firecrawl"
|
|
|
|
def test_invalid_config_falls_through_to_fallback(self):
|
|
"""web.backend=invalid → ignored, uses key-based fallback."""
|
|
from hermes_agent.tools.web import _get_backend
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "nonexistent"}), \
|
|
patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
assert _get_backend() == "parallel"
|
|
|
|
|
|
class TestParallelClientConfig:
|
|
"""Test suite for Parallel client initialization."""
|
|
|
|
def setup_method(self):
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
web_tools_mod._parallel_client = None
|
|
os.environ.pop("PARALLEL_API_KEY", None)
|
|
fake_parallel = types.ModuleType("parallel")
|
|
|
|
class Parallel:
|
|
def __init__(self, api_key):
|
|
self.api_key = api_key
|
|
|
|
class AsyncParallel:
|
|
def __init__(self, api_key):
|
|
self.api_key = api_key
|
|
|
|
fake_parallel.Parallel = Parallel
|
|
fake_parallel.AsyncParallel = AsyncParallel
|
|
sys.modules["parallel"] = fake_parallel
|
|
|
|
def teardown_method(self):
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
web_tools_mod._parallel_client = None
|
|
os.environ.pop("PARALLEL_API_KEY", None)
|
|
sys.modules.pop("parallel", None)
|
|
|
|
def test_creates_client_with_key(self):
|
|
"""PARALLEL_API_KEY set → creates Parallel client."""
|
|
with patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
from hermes_agent.tools.web import _get_parallel_client
|
|
from parallel import Parallel
|
|
client = _get_parallel_client()
|
|
assert client is not None
|
|
assert isinstance(client, Parallel)
|
|
|
|
def test_no_key_raises_with_helpful_message(self):
|
|
"""No PARALLEL_API_KEY → ValueError with guidance."""
|
|
from hermes_agent.tools.web import _get_parallel_client
|
|
with pytest.raises(ValueError, match="PARALLEL_API_KEY"):
|
|
_get_parallel_client()
|
|
|
|
def test_singleton_returns_same_instance(self):
|
|
"""Second call returns cached client."""
|
|
with patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
from hermes_agent.tools.web import _get_parallel_client
|
|
client1 = _get_parallel_client()
|
|
client2 = _get_parallel_client()
|
|
assert client1 is client2
|
|
|
|
|
|
class TestWebSearchErrorHandling:
|
|
"""Test suite for web_search_tool() error responses."""
|
|
|
|
def test_search_error_response_does_not_expose_diagnostics(self):
|
|
from hermes_agent.tools import web as web_tools_mod
|
|
|
|
firecrawl_client = MagicMock()
|
|
firecrawl_client.search.side_effect = RuntimeError("boom")
|
|
|
|
with patch("hermes_agent.tools.web._get_backend", return_value="firecrawl"), \
|
|
patch("hermes_agent.tools.web._get_firecrawl_client", return_value=firecrawl_client), \
|
|
patch("hermes_agent.tools.interrupt.is_interrupted", return_value=False), \
|
|
patch.object(web_tools_mod._debug, "log_call") as mock_log_call, \
|
|
patch.object(web_tools_mod._debug, "save"):
|
|
result = json.loads(web_tools_mod.web_search_tool("test query", limit=3))
|
|
|
|
assert result == {"error": "Error searching web: boom"}
|
|
|
|
debug_payload = mock_log_call.call_args.args[1]
|
|
assert debug_payload["error"] == "Error searching web: boom"
|
|
assert "traceback" not in debug_payload["error"]
|
|
assert "exception_type" not in debug_payload["error"]
|
|
assert "config" not in result
|
|
assert "exception_type" not in result
|
|
assert "exception_chain" not in result
|
|
assert "traceback" not in result
|
|
|
|
|
|
class TestCheckWebApiKey:
|
|
"""Test suite for check_web_api_key() unified availability check."""
|
|
|
|
_ENV_KEYS = (
|
|
"EXA_API_KEY",
|
|
"PARALLEL_API_KEY",
|
|
"FIRECRAWL_API_KEY",
|
|
"FIRECRAWL_API_URL",
|
|
"FIRECRAWL_GATEWAY_URL",
|
|
"TOOL_GATEWAY_DOMAIN",
|
|
"TOOL_GATEWAY_SCHEME",
|
|
"TOOL_GATEWAY_USER_TOKEN",
|
|
"TAVILY_API_KEY",
|
|
)
|
|
|
|
def setup_method(self):
|
|
for key in self._ENV_KEYS:
|
|
os.environ.pop(key, None)
|
|
self._managed_patchers = [
|
|
patch("hermes_agent.tools.web.managed_nous_tools_enabled", return_value=True),
|
|
patch("hermes_agent.tools.managed_gateway.managed_nous_tools_enabled", return_value=True),
|
|
]
|
|
for p in self._managed_patchers:
|
|
p.start()
|
|
|
|
def teardown_method(self):
|
|
for key in self._ENV_KEYS:
|
|
os.environ.pop(key, None)
|
|
for p in self._managed_patchers:
|
|
p.stop()
|
|
|
|
def test_parallel_key_only(self):
|
|
with patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_exa_key_only(self):
|
|
with patch.dict(os.environ, {"EXA_API_KEY": "exa-test"}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_firecrawl_key_only(self):
|
|
with patch.dict(os.environ, {"FIRECRAWL_API_KEY": "fc-test"}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_firecrawl_url_only(self):
|
|
with patch.dict(os.environ, {"FIRECRAWL_API_URL": "http://localhost:3002"}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_tavily_key_only(self):
|
|
with patch.dict(os.environ, {"TAVILY_API_KEY": "tvly-test"}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_no_keys_returns_false(self):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is False
|
|
|
|
def test_both_keys_returns_true(self):
|
|
with patch.dict(os.environ, {
|
|
"PARALLEL_API_KEY": "test-key",
|
|
"FIRECRAWL_API_KEY": "fc-test",
|
|
}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_all_three_keys_returns_true(self):
|
|
with patch.dict(os.environ, {
|
|
"PARALLEL_API_KEY": "test-key",
|
|
"FIRECRAWL_API_KEY": "fc-test",
|
|
"TAVILY_API_KEY": "tvly-test",
|
|
}):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_tool_gateway_returns_true(self):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
def test_configured_backend_must_match_available_provider(self):
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "parallel"}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch.dict(os.environ, {"FIRECRAWL_GATEWAY_URL": "http://127.0.0.1:3002"}, clear=False):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is False
|
|
|
|
def test_configured_firecrawl_backend_accepts_managed_gateway(self):
|
|
with patch("hermes_agent.tools.web._load_web_config", return_value={"backend": "firecrawl"}):
|
|
with patch("hermes_agent.tools.web._read_nous_access_token", return_value="nous-token"):
|
|
with patch.dict(os.environ, {"FIRECRAWL_GATEWAY_URL": "http://127.0.0.1:3002"}, clear=False):
|
|
from hermes_agent.tools.web import check_web_api_key
|
|
assert check_web_api_key() is True
|
|
|
|
|
|
def test_web_requires_env_includes_exa_key():
|
|
from hermes_agent.tools.web import _web_requires_env
|
|
|
|
assert "EXA_API_KEY" in _web_requires_env()
|