Merge branch 'main' into rewbs/tool-use-charge-to-subscription

This commit is contained in:
Robin Fernandes
2026-03-31 09:29:43 +09:00
160 changed files with 5545 additions and 664 deletions

View File

@@ -0,0 +1,42 @@
"""Tests for the top-level `./hermes` launcher script."""
import runpy
import sys
import types
from pathlib import Path
def test_launcher_delegates_to_argparse_entrypoint(monkeypatch):
"""`./hermes` should use `hermes_cli.main`, not the legacy Fire wrapper."""
launcher_path = Path(__file__).resolve().parents[2] / "hermes"
called = []
fake_main_module = types.ModuleType("hermes_cli.main")
def fake_main():
called.append("hermes_cli.main")
fake_main_module.main = fake_main
monkeypatch.setitem(sys.modules, "hermes_cli.main", fake_main_module)
fake_cli_module = types.ModuleType("cli")
def legacy_cli_main(*args, **kwargs):
raise AssertionError("launcher should not import cli.main")
fake_cli_module.main = legacy_cli_main
monkeypatch.setitem(sys.modules, "cli", fake_cli_module)
fake_fire_module = types.ModuleType("fire")
def legacy_fire(*args, **kwargs):
raise AssertionError("launcher should not invoke fire.Fire")
fake_fire_module.Fire = legacy_fire
monkeypatch.setitem(sys.modules, "fire", fake_fire_module)
monkeypatch.setattr(sys, "argv", [str(launcher_path), "gateway", "status"])
runpy.run_path(str(launcher_path), run_name="__main__")
assert called == ["hermes_cli.main"]

View File

@@ -42,3 +42,55 @@ def test_get_nous_subscription_features_prefers_managed_modal_in_auto_mode(monke
assert features.modal.active is True
assert features.modal.managed_by_nous is True
assert features.modal.direct_override is False
def test_get_nous_subscription_features_prefers_camofox_over_managed_browserbase(monkeypatch):
env = {"CAMOFOX_URL": "http://localhost:9377"}
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
monkeypatch.setattr(
ns,
"is_managed_tool_gateway_ready",
lambda vendor: vendor == "browserbase",
)
features = ns.get_nous_subscription_features(
{"browser": {"cloud_provider": "browserbase"}}
)
assert features.browser.available is True
assert features.browser.active is True
assert features.browser.managed_by_nous is False
assert features.browser.direct_override is True
assert features.browser.current_provider == "Camofox"
def test_get_nous_subscription_features_requires_agent_browser_for_browserbase(monkeypatch):
env = {
"BROWSERBASE_API_KEY": "bb-key",
"BROWSERBASE_PROJECT_ID": "bb-project",
}
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {})
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: False)
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: False)
features = ns.get_nous_subscription_features(
{"browser": {"cloud_provider": "browserbase"}}
)
assert features.browser.available is False
assert features.browser.active is False
assert features.browser.managed_by_nous is False
assert features.browser.current_provider == "Browserbase"

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from hermes_cli.config import load_config, save_config, save_env_value
from hermes_cli.nous_subscription import NousFeatureState, NousSubscriptionFeatures
from hermes_cli.setup import _print_setup_summary, setup_model_provider
@@ -471,3 +472,58 @@ def test_setup_summary_marks_anthropic_auth_as_vision_available(tmp_path, monkey
assert "Vision (image analysis)" in output
assert "missing run 'hermes setup' to configure" not in output
def test_setup_summary_shows_camofox_when_browser_feature_is_camofox(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
_clear_provider_env(monkeypatch)
monkeypatch.setattr(
"hermes_cli.setup.get_nous_subscription_features",
lambda config: NousSubscriptionFeatures(
subscribed=False,
nous_auth_present=False,
provider_is_nous=False,
features={
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
"browser": NousFeatureState("browser", "Browser automation", True, True, True, False, True, True, "Camofox"),
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, "local"),
},
),
)
monkeypatch.setattr("agent.auxiliary_client.get_available_vision_backends", lambda: [])
_print_setup_summary(load_config(), tmp_path)
output = capsys.readouterr().out
assert "Browser Automation (Camofox)" in output
def test_setup_summary_does_not_mark_incomplete_browserbase_as_available(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
_clear_provider_env(monkeypatch)
monkeypatch.setenv("BROWSERBASE_API_KEY", "bb-key")
monkeypatch.setattr(
"hermes_cli.setup.get_nous_subscription_features",
lambda config: NousSubscriptionFeatures(
subscribed=False,
nous_auth_present=False,
provider_is_nous=False,
features={
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
"browser": NousFeatureState("browser", "Browser automation", True, False, False, False, False, True, "Browserbase"),
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, "local"),
},
),
)
monkeypatch.setattr("agent.auxiliary_client.get_available_vision_backends", lambda: [])
_print_setup_summary(load_config(), tmp_path)
output = capsys.readouterr().out
assert "Browser Automation (Browserbase)" not in output
assert "Browser Automation" in output
assert "BROWSERBASE_API_KEY/BROWSERBASE_PROJECT_ID" in output

View File

@@ -0,0 +1,44 @@
"""Tests for subprocess.run() timeout coverage in CLI utilities."""
import ast
from pathlib import Path
import pytest
# Parameterise over every CLI module that calls subprocess.run
_CLI_MODULES = [
"hermes_cli/doctor.py",
"hermes_cli/status.py",
"hermes_cli/clipboard.py",
"hermes_cli/banner.py",
]
def _subprocess_run_calls(filepath: str) -> list[dict]:
"""Parse a Python file and return info about subprocess.run() calls."""
source = Path(filepath).read_text()
tree = ast.parse(source, filename=filepath)
calls = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if (isinstance(func, ast.Attribute) and func.attr == "run"
and isinstance(func.value, ast.Name)
and func.value.id == "subprocess"):
has_timeout = any(kw.arg == "timeout" for kw in node.keywords)
calls.append({"line": node.lineno, "has_timeout": has_timeout})
return calls
@pytest.mark.parametrize("filepath", _CLI_MODULES)
def test_all_subprocess_run_calls_have_timeout(filepath):
"""Every subprocess.run() call in CLI modules must specify a timeout."""
if not Path(filepath).exists():
pytest.skip(f"{filepath} not found")
calls = _subprocess_run_calls(filepath)
missing = [c for c in calls if not c["has_timeout"]]
assert not missing, (
f"{filepath} has subprocess.run() without timeout at "
f"line(s): {[c['line'] for c in missing]}"
)