mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
* fix(tests): mock is_safe_url in tests that use example.com Tests using example.com URLs were failing because is_safe_url does a real DNS lookup which fails in environments where example.com doesn't resolve, causing the request to be blocked before reaching the already-mocked HTTP client. This should fix around 17 failing tests. These tests test logic, caching, etc. so mocking this method should not modify them in any way. TestMattermostSendUrlAsFile was already doing this so we follow the same pattern. * fix(test): use case-insensitive lookup for model context length check DEFAULT_CONTEXT_LENGTHS uses inconsistent casing (MiniMax keys are lowercase, Qwen keys are mixed-case) so the test was broken in some cases since it couldn't find the model. * fix(test): patch is_linux in systemd gateway restart test The test only patched is_macos to False but didn't patch is_linux to True. On macOS hosts, is_linux() returns False and the systemd restart code path is skipped entirely, making the assertion fail. * fix(test): use non-blocklisted env var in docker forward_env tests GITHUB_TOKEN is in api_key_env_vars and thus in _HERMES_PROVIDER_ENV_BLOCKLIST so the env var is silently dropped, we replace it with a non-blocked one like DATABASE_URL so the tests actually work. * fix(test): fully isolate _has_any_provider_configured from host env _has_any_provider_configured() checks all env vars from PROVIDER_REGISTRY (not just the 5 the tests were clearing) and also calls get_auth_status() which detects gh auth token for Copilot. On machines with any of these set, the function returns True before reaching the code path under test. Clear all registry vars and mock get_auth_status so host credentials don't interfere. * fix(test): correct path to hermes_base_env.py in tool parser tests Path(__file__).parent.parent resolved to tests/, not the project root. The file lives at environments/hermes_base_env.py so we need one more parent level. * fix(test): accept optional HTML fields in Matrix send payload _send_matrix sometimes adds format and formatted_body when the markdown library is installed. The test was doing an exact dict equality check which broke. Check required fields instead. * fix(test): add config.yaml to codex vision requirements test The test only wrote auth.json but not config.yaml, so _read_main_provider() returned empty and vision auto-detect never tried the codex provider. Add a config.yaml pointing at openai-codex so the fallback path actually resolves the client. * fix(test): clear OPENROUTER_API_KEY in _isolate_hermes_home run_agent.py calls load_hermes_dotenv() at import time, which injects API keys from ~/.hermes/.env into os.environ before any test fixture runs. This caused test_agent_loop_tool_calling to make real API calls instead of skipping, which ends up making some tests fail. * fix(test): add get_rate_limit_state to agent mock in usage report tests _show_usage now calls agent.get_rate_limit_state() for rate limit display. The SimpleNamespace mock was missing this method. * fix(test): update expected Camofox config version from 12 to 13 * fix(test): mock _get_enabled_platforms in nous managed defaults test Importing gateway.run leaks DISCORD_BOT_TOKEN into os.environ, which makes _get_enabled_platforms() return ["cli", "discord"] instead of just ["cli"]. tools_command loops per platform, so apply_nous_managed_defaults runs twice: the first call sets config values, the second sees them as already configured and returns an empty set, causing the assertion to fail.
178 lines
6.9 KiB
Python
178 lines
6.9 KiB
Python
"""
|
|
Tests for ManagedServer / tool-parser integration.
|
|
|
|
Validates that:
|
|
1. The installed atroposlib API still matches Hermes's expectations
|
|
2. Hermes's parser registry remains compatible with ManagedServer parsing
|
|
3. HermesAgentBaseEnv wires the selected parser into ServerManager correctly
|
|
|
|
These tests verify the contract between hermes-agent's environments/ code
|
|
and atroposlib's ManagedServer. They detect API incompatibilities early.
|
|
"""
|
|
|
|
import inspect
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
try:
|
|
import atroposlib # noqa: F401
|
|
except ImportError:
|
|
pytest.skip("atroposlib not installed", allow_module_level=True)
|
|
|
|
|
|
class TestManagedServerAPI:
|
|
"""Test that ManagedServer's API matches what hermes-agent expects."""
|
|
|
|
def test_managed_server_init_signature(self):
|
|
"""ManagedServer should accept tool_call_parser parameter."""
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
sig = inspect.signature(ManagedServer.__init__)
|
|
params = list(sig.parameters.keys())
|
|
|
|
# Core params that must exist
|
|
assert "self" in params
|
|
assert "server" in params
|
|
assert "tokenizer" in params
|
|
assert "track_tree" in params
|
|
|
|
# tool_call_parser — required for tool_call_support branch
|
|
# If this fails, atroposlib hasn't been updated to tool_call_support
|
|
has_tool_parser = "tool_call_parser" in params
|
|
if not has_tool_parser:
|
|
pytest.skip(
|
|
"ManagedServer does not have tool_call_parser param — "
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
)
|
|
|
|
def test_server_manager_managed_server_signature(self):
|
|
"""ServerManager.managed_server() should accept tool_call_parser."""
|
|
from atroposlib.envs.server_handling.server_manager import ServerManager
|
|
|
|
sig = inspect.signature(ServerManager.managed_server)
|
|
params = list(sig.parameters.keys())
|
|
|
|
assert "self" in params
|
|
assert "tokenizer" in params
|
|
|
|
has_tool_parser = "tool_call_parser" in params
|
|
if not has_tool_parser:
|
|
pytest.skip(
|
|
"ServerManager.managed_server() does not have tool_call_parser param — "
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
)
|
|
|
|
def test_managed_server_chat_template_kwargs(self):
|
|
"""ManagedServer should have CHAT_TEMPLATE_KWARGS for forwarding tools/thinking."""
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
if not hasattr(ManagedServer, "CHAT_TEMPLATE_KWARGS"):
|
|
pytest.skip(
|
|
"ManagedServer does not have CHAT_TEMPLATE_KWARGS — "
|
|
"baseline atroposlib (pre tool_call_support branch)"
|
|
)
|
|
|
|
kwargs = ManagedServer.CHAT_TEMPLATE_KWARGS
|
|
assert "tools" in kwargs, "tools must be in CHAT_TEMPLATE_KWARGS"
|
|
|
|
def test_no_get_logprobs_method(self):
|
|
"""get_logprobs should be removed in tool_call_support branch."""
|
|
from atroposlib.envs.server_handling.managed_server import ManagedServer
|
|
|
|
# In baseline, get_logprobs exists. In tool_call_support, it's removed.
|
|
# We just note the state — not a hard fail either way.
|
|
has_get_logprobs = hasattr(ManagedServer, "get_logprobs")
|
|
if has_get_logprobs:
|
|
pytest.skip(
|
|
"ManagedServer still has get_logprobs — baseline atroposlib"
|
|
)
|
|
|
|
|
|
class TestParserCompatibility:
|
|
"""Test that hermes-agent's parsers match ManagedServer's expectations."""
|
|
|
|
def test_parser_parse_returns_correct_format(self):
|
|
"""
|
|
ManagedServer expects parser.parse(text) -> (content, tool_calls)
|
|
where tool_calls is a list of objects with .id, .function.name, .function.arguments
|
|
"""
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
parser = get_parser("hermes")
|
|
text = '<tool_call>{"name": "terminal", "arguments": {"command": "ls"}}</tool_call>'
|
|
content, tool_calls = parser.parse(text)
|
|
|
|
assert tool_calls is not None
|
|
assert len(tool_calls) == 1
|
|
|
|
tc = tool_calls[0]
|
|
# ManagedServer accesses these attrs directly
|
|
assert hasattr(tc, "id")
|
|
assert hasattr(tc, "function")
|
|
assert hasattr(tc.function, "name")
|
|
assert hasattr(tc.function, "arguments")
|
|
|
|
def test_parser_no_tools_returns_none(self):
|
|
"""ManagedServer checks `if parsed_tool_calls:` — None should be falsy."""
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
parser = get_parser("hermes")
|
|
content, tool_calls = parser.parse("Just text, no tools")
|
|
assert tool_calls is None
|
|
|
|
def test_parser_content_is_string_or_none(self):
|
|
"""ManagedServer uses `parsed_content or ""` — must be str or None."""
|
|
from environments.tool_call_parsers import get_parser
|
|
|
|
parser = get_parser("hermes")
|
|
|
|
# With tool calls
|
|
text = '<tool_call>{"name": "terminal", "arguments": {"command": "ls"}}</tool_call>'
|
|
content, _ = parser.parse(text)
|
|
assert content is None or isinstance(content, str)
|
|
|
|
# Without tool calls
|
|
content2, _ = parser.parse("Just text")
|
|
assert isinstance(content2, str)
|
|
|
|
|
|
class TestBaseEnvCompatibility:
|
|
"""Test that hermes_base_env.py's tool-parser wiring matches the current API."""
|
|
|
|
def test_hermes_base_env_sets_server_manager_tool_parser(self):
|
|
"""Hermes wires parser selection through ServerManager.tool_parser."""
|
|
import ast
|
|
|
|
base_env_path = Path(__file__).parent.parent.parent / "environments" / "hermes_base_env.py"
|
|
source = base_env_path.read_text()
|
|
tree = ast.parse(source)
|
|
|
|
found_assignment = False
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Assign):
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Attribute) and target.attr == "tool_parser":
|
|
parent = target.value
|
|
if (
|
|
isinstance(parent, ast.Attribute)
|
|
and parent.attr == "server"
|
|
and isinstance(parent.value, ast.Name)
|
|
and parent.value.id == "self"
|
|
):
|
|
found_assignment = True
|
|
|
|
assert found_assignment, (
|
|
"hermes_base_env.py should set self.server.tool_parser from config.tool_call_parser"
|
|
)
|
|
|
|
def test_hermes_base_env_uses_config_tool_call_parser(self):
|
|
"""Verify hermes_base_env uses the config field rather than a local parser instance."""
|
|
base_env_path = Path(__file__).parent.parent.parent / "environments" / "hermes_base_env.py"
|
|
source = base_env_path.read_text()
|
|
|
|
assert 'tool_call_parser: str = Field(' in source
|
|
assert 'self.server.tool_parser = config.tool_call_parser' in source
|