Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
@@ -951,13 +951,21 @@ class TestBuildAnthropicKwargs:
|
||||
max_tokens=4096,
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
)
|
||||
assert kwargs["thinking"] == {"type": "adaptive"}
|
||||
# Adaptive thinking + display="summarized" keeps reasoning text
|
||||
# populated in the response stream (Opus 4.7 default is "omitted").
|
||||
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||
assert kwargs["output_config"] == {"effort": "high"}
|
||||
assert "budget_tokens" not in kwargs["thinking"]
|
||||
assert "temperature" not in kwargs
|
||||
assert kwargs["max_tokens"] == 4096
|
||||
|
||||
def test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models(self):
|
||||
def test_reasoning_config_downgrades_xhigh_to_max_for_4_6_models(self):
|
||||
# Opus 4.7 added "xhigh" as a distinct effort level (low/medium/high/
|
||||
# xhigh/max). Opus 4.6 only supports low/medium/high/max — sending
|
||||
# "xhigh" there returns an API 400. Preserve the pre-migration
|
||||
# behavior of aliasing xhigh→max on pre-4.7 adaptive models so users
|
||||
# who prefer xhigh as their default don't 400 every request when
|
||||
# switching back to 4.6.
|
||||
kwargs = build_anthropic_kwargs(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "think harder"}],
|
||||
@@ -965,9 +973,53 @@ class TestBuildAnthropicKwargs:
|
||||
max_tokens=4096,
|
||||
reasoning_config={"enabled": True, "effort": "xhigh"},
|
||||
)
|
||||
assert kwargs["thinking"] == {"type": "adaptive"}
|
||||
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||
assert kwargs["output_config"] == {"effort": "max"}
|
||||
|
||||
def test_reasoning_config_preserves_xhigh_for_4_7_models(self):
|
||||
# On 4.7+ xhigh is a real level and the recommended default for
|
||||
# coding/agentic work — keep it distinct from max.
|
||||
kwargs = build_anthropic_kwargs(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "think harder"}],
|
||||
tools=None,
|
||||
max_tokens=4096,
|
||||
reasoning_config={"enabled": True, "effort": "xhigh"},
|
||||
)
|
||||
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||
assert kwargs["output_config"] == {"effort": "xhigh"}
|
||||
|
||||
def test_reasoning_config_maps_max_effort_for_4_7_models(self):
|
||||
kwargs = build_anthropic_kwargs(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "maximum reasoning please"}],
|
||||
tools=None,
|
||||
max_tokens=4096,
|
||||
reasoning_config={"enabled": True, "effort": "max"},
|
||||
)
|
||||
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||
assert kwargs["output_config"] == {"effort": "max"}
|
||||
|
||||
def test_opus_4_7_strips_sampling_params(self):
|
||||
# Opus 4.7 returns 400 on non-default temperature/top_p/top_k.
|
||||
# build_anthropic_kwargs must strip them as a safety net even if an
|
||||
# upstream caller injects them for older-model compatibility.
|
||||
kwargs = build_anthropic_kwargs(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
reasoning_config=None,
|
||||
)
|
||||
# Manually inject sampling params then re-run through the guard.
|
||||
# Because build_anthropic_kwargs doesn't currently accept sampling
|
||||
# params through its signature, we exercise the strip behavior by
|
||||
# calling the internal predicate directly.
|
||||
from agent.anthropic_adapter import _forbids_sampling_params
|
||||
assert _forbids_sampling_params("claude-opus-4-7") is True
|
||||
assert _forbids_sampling_params("claude-opus-4-6") is False
|
||||
assert _forbids_sampling_params("claude-sonnet-4-5") is False
|
||||
|
||||
def test_reasoning_disabled(self):
|
||||
kwargs = build_anthropic_kwargs(
|
||||
model="claude-sonnet-4-20250514",
|
||||
@@ -1248,6 +1300,21 @@ class TestNormalizeResponse:
|
||||
assert r2 == "tool_calls"
|
||||
assert r3 == "length"
|
||||
|
||||
def test_stop_reason_refusal_and_context_exceeded(self):
|
||||
# Claude 4.5+ introduced two new stop_reason values the Messages API
|
||||
# returns. We map both to OpenAI-style finish_reasons upstream
|
||||
# handlers already understand, instead of silently collapsing to
|
||||
# "stop" (old behavior).
|
||||
block = SimpleNamespace(type="text", text="")
|
||||
_, refusal_reason = normalize_anthropic_response(
|
||||
self._make_response([block], "refusal")
|
||||
)
|
||||
_, overflow_reason = normalize_anthropic_response(
|
||||
self._make_response([block], "model_context_window_exceeded")
|
||||
)
|
||||
assert refusal_reason == "content_filter"
|
||||
assert overflow_reason == "length"
|
||||
|
||||
def test_no_text_content(self):
|
||||
block = SimpleNamespace(
|
||||
type="tool_use", id="tc_1", name="search", input={"q": "hi"}
|
||||
|
||||
@@ -113,8 +113,10 @@ class TestDefaultContextLengths:
|
||||
for key, value in DEFAULT_CONTEXT_LENGTHS.items():
|
||||
if "claude" not in key:
|
||||
continue
|
||||
# Claude 4.6 models have 1M context
|
||||
if "4.6" in key or "4-6" in key:
|
||||
# Claude 4.6+ models (4.6 and 4.7) have 1M context at standard
|
||||
# API pricing (no long-context premium). Older Claude 4.x and
|
||||
# 3.x models cap at 200k.
|
||||
if any(tag in key for tag in ("4.6", "4-6", "4.7", "4-7")):
|
||||
assert value == 1000000, f"{key} should be 1000000"
|
||||
else:
|
||||
assert value == 200000, f"{key} should be 200000"
|
||||
|
||||
@@ -413,7 +413,7 @@ class TestBuildSkillsSystemPrompt:
|
||||
|
||||
class TestBuildNousSubscriptionPrompt:
|
||||
def test_includes_active_subscription_features(self, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_subscription_features",
|
||||
lambda config=None: NousSubscriptionFeatures(
|
||||
@@ -437,7 +437,7 @@ class TestBuildNousSubscriptionPrompt:
|
||||
assert "do not ask the user for Firecrawl, FAL, OpenAI TTS, or Browser-Use API keys" in prompt
|
||||
|
||||
def test_non_subscriber_prompt_includes_relevant_upgrade_guidance(self, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_subscription_features",
|
||||
lambda config=None: NousSubscriptionFeatures(
|
||||
@@ -460,7 +460,7 @@ class TestBuildNousSubscriptionPrompt:
|
||||
assert "Do not mention subscription unless" in prompt
|
||||
|
||||
def test_feature_flag_off_returns_empty_prompt(self, monkeypatch):
|
||||
monkeypatch.delenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", raising=False)
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: False)
|
||||
|
||||
prompt = build_nous_subscription_prompt({"web_search"})
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ def test_codex_provider_replaces_incompatible_default_model(monkeypatch):
|
||||
|
||||
|
||||
def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_tts(monkeypatch, capsys):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
|
||||
config = {
|
||||
"model": {"provider": "nous", "default": "claude-opus-4-6"},
|
||||
"tts": {"provider": "elevenlabs"},
|
||||
@@ -333,21 +333,17 @@ def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
|
||||
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_subscription_explainer_lines",
|
||||
lambda: ["Nous subscription enables managed web tools."],
|
||||
)
|
||||
|
||||
hermes_main._model_flow_nous(config, current_model="claude-opus-4-6")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Nous subscription enables managed web tools." in out
|
||||
assert "Default model set to:" in out
|
||||
assert config["tts"]["provider"] == "elevenlabs"
|
||||
assert config["browser"]["cloud_provider"] == "browser-use"
|
||||
|
||||
|
||||
def test_model_flow_nous_applies_managed_tts_default_when_unconfigured(monkeypatch, capsys):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
def test_model_flow_nous_offers_tool_gateway_prompt_when_unconfigured(monkeypatch, capsys):
|
||||
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
|
||||
config = {
|
||||
"model": {"provider": "nous", "default": "claude-opus-4-6"},
|
||||
"tts": {"provider": "edge"},
|
||||
@@ -355,13 +351,13 @@ def test_model_flow_nous_applies_managed_tts_default_when_unconfigured(monkeypat
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_provider_auth_state",
|
||||
lambda provider: {"access_token": "nous-token"},
|
||||
lambda provider: {"access_token": "***"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.resolve_nous_runtime_credentials",
|
||||
lambda *args, **kwargs: {
|
||||
"base_url": "https://inference.example.com/v1",
|
||||
"api_key": "nous-key",
|
||||
"api_key": "***",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -371,17 +367,12 @@ def test_model_flow_nous_applies_managed_tts_default_when_unconfigured(monkeypat
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
|
||||
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.nous_subscription.get_nous_subscription_explainer_lines",
|
||||
lambda: ["Nous subscription enables managed web tools."],
|
||||
)
|
||||
|
||||
hermes_main._model_flow_nous(config, current_model="claude-opus-4-6")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Nous subscription enables managed web tools." in out
|
||||
assert "OpenAI TTS via your Nous subscription" in out
|
||||
assert config["tts"]["provider"] == "openai"
|
||||
# Tool Gateway prompt should be shown (input() raises OSError in pytest
|
||||
# which is caught, so the prompt text appears but nothing is applied)
|
||||
assert "Tool Gateway" in out
|
||||
|
||||
|
||||
def test_codex_provider_uses_config_model(monkeypatch):
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_get_nous_subscription_features_recognizes_direct_exa_backend(monkeypatc
|
||||
|
||||
|
||||
def test_get_nous_subscription_features_prefers_managed_modal_in_auto_mode(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
||||
monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True)
|
||||
|
||||
@@ -363,7 +363,7 @@ def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, mon
|
||||
|
||||
|
||||
def test_modal_setup_can_use_nous_subscription_without_modal_creds(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.setup.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config = load_config()
|
||||
|
||||
@@ -405,7 +405,7 @@ def test_modal_setup_can_use_nous_subscription_without_modal_creds(tmp_path, mon
|
||||
|
||||
|
||||
def test_modal_setup_persists_direct_mode_when_user_chooses_their_own_account(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.setup.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.delenv("MODAL_TOKEN_ID", raising=False)
|
||||
monkeypatch.delenv("MODAL_TOKEN_SECRET", raising=False)
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_show_status_displays_legacy_string_model_and_custom_endpoint(monkeypatc
|
||||
|
||||
|
||||
def test_show_status_reports_managed_nous_features(monkeypatch, capsys, tmp_path):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.status.managed_nous_tools_enabled", lambda: True)
|
||||
from hermes_cli import status as status_mod
|
||||
|
||||
_patch_common_status_deps(monkeypatch, status_mod, tmp_path)
|
||||
@@ -98,13 +98,13 @@ def test_show_status_reports_managed_nous_features(monkeypatch, capsys, tmp_path
|
||||
status_mod.show_status(SimpleNamespace(all=False, deep=False))
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Nous Subscription Features" in out
|
||||
assert "Nous Tool Gateway" in out
|
||||
assert "Browser automation" in out
|
||||
assert "active via Nous subscription" in out
|
||||
|
||||
|
||||
def test_show_status_hides_nous_subscription_section_when_feature_flag_is_off(monkeypatch, capsys, tmp_path):
|
||||
monkeypatch.delenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", raising=False)
|
||||
monkeypatch.setattr("hermes_cli.status.managed_nous_tools_enabled", lambda: False)
|
||||
from hermes_cli import status as status_mod
|
||||
|
||||
_patch_common_status_deps(monkeypatch, status_mod, tmp_path)
|
||||
@@ -121,4 +121,4 @@ def test_show_status_hides_nous_subscription_section_when_feature_flag_is_off(mo
|
||||
status_mod.show_status(SimpleNamespace(all=False, deep=False))
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Nous Subscription Features" not in out
|
||||
assert "Nous Tool Gateway" not in out
|
||||
|
||||
@@ -296,7 +296,7 @@ def test_save_platform_tools_still_preserves_mcp_with_platform_default_present()
|
||||
|
||||
|
||||
def test_visible_providers_include_nous_subscription_when_logged_in(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.tools_config.managed_nous_tools_enabled", lambda: True)
|
||||
config = {"model": {"provider": "nous"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
@@ -310,7 +310,7 @@ def test_visible_providers_include_nous_subscription_when_logged_in(monkeypatch)
|
||||
|
||||
|
||||
def test_visible_providers_hide_nous_subscription_when_feature_flag_is_off(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", raising=False)
|
||||
monkeypatch.setattr("hermes_cli.tools_config.managed_nous_tools_enabled", lambda: False)
|
||||
config = {"model": {"provider": "nous"}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
@@ -338,7 +338,8 @@ def test_local_browser_provider_is_saved_explicitly(monkeypatch):
|
||||
|
||||
|
||||
def test_first_install_nous_auto_configures_managed_defaults(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("hermes_cli.tools_config.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
|
||||
config = {
|
||||
"model": {"provider": "nous"},
|
||||
"platform_toolsets": {"cli": []},
|
||||
|
||||
@@ -366,6 +366,17 @@ class TestPeerLookupHelpers:
|
||||
|
||||
|
||||
class TestConcludeToolDispatch:
|
||||
def test_conclude_schema_has_no_anyof(self):
|
||||
"""anyOf/oneOf/allOf breaks Anthropic and Fireworks APIs — schema must be plain object."""
|
||||
from plugins.memory.honcho import CONCLUDE_SCHEMA
|
||||
params = CONCLUDE_SCHEMA["parameters"]
|
||||
assert params["type"] == "object"
|
||||
assert "conclusion" in params["properties"]
|
||||
assert "delete_id" in params["properties"]
|
||||
assert "anyOf" not in params
|
||||
assert "oneOf" not in params
|
||||
assert "allOf" not in params
|
||||
|
||||
def test_honcho_conclude_defaults_to_user_peer(self):
|
||||
provider = HonchoMemoryProvider()
|
||||
provider._session_initialized = True
|
||||
@@ -470,10 +481,50 @@ class TestConcludeToolDispatch:
|
||||
result = provider.handle_tool_call("honcho_conclude", {})
|
||||
|
||||
parsed = json.loads(result)
|
||||
assert "error" in parsed or "Missing required" in parsed.get("result", "")
|
||||
assert parsed == {"error": "Exactly one of conclusion or delete_id must be provided."}
|
||||
provider._manager.create_conclusion.assert_not_called()
|
||||
provider._manager.delete_conclusion.assert_not_called()
|
||||
|
||||
def test_honcho_conclude_rejects_both_params_at_once(self):
|
||||
"""Sending both conclusion and delete_id should be rejected."""
|
||||
import json
|
||||
provider = HonchoMemoryProvider()
|
||||
provider._session_initialized = True
|
||||
provider._session_key = "telegram:123"
|
||||
provider._manager = MagicMock()
|
||||
result = provider.handle_tool_call(
|
||||
"honcho_conclude",
|
||||
{"conclusion": "User prefers dark mode", "delete_id": "conc-123"},
|
||||
)
|
||||
parsed = json.loads(result)
|
||||
assert parsed == {"error": "Exactly one of conclusion or delete_id must be provided."}
|
||||
provider._manager.create_conclusion.assert_not_called()
|
||||
provider._manager.delete_conclusion.assert_not_called()
|
||||
|
||||
def test_honcho_conclude_rejects_whitespace_only_conclusion(self):
|
||||
"""Whitespace-only conclusion should be treated as empty."""
|
||||
import json
|
||||
provider = HonchoMemoryProvider()
|
||||
provider._session_initialized = True
|
||||
provider._session_key = "telegram:123"
|
||||
provider._manager = MagicMock()
|
||||
result = provider.handle_tool_call("honcho_conclude", {"conclusion": " "})
|
||||
parsed = json.loads(result)
|
||||
assert parsed == {"error": "Exactly one of conclusion or delete_id must be provided."}
|
||||
provider._manager.create_conclusion.assert_not_called()
|
||||
|
||||
def test_honcho_conclude_rejects_whitespace_only_delete_id(self):
|
||||
"""Whitespace-only delete_id should be treated as empty."""
|
||||
import json
|
||||
provider = HonchoMemoryProvider()
|
||||
provider._session_initialized = True
|
||||
provider._session_key = "telegram:123"
|
||||
provider._manager = MagicMock()
|
||||
result = provider.handle_tool_call("honcho_conclude", {"delete_id": " "})
|
||||
parsed = json.loads(result)
|
||||
assert parsed == {"error": "Exactly one of conclusion or delete_id must be provided."}
|
||||
provider._manager.delete_conclusion.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Message chunking
|
||||
|
||||
@@ -47,7 +47,15 @@ def _restore_tool_and_agent_modules():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_managed_nous_tools(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
"""Ensure managed_nous_tools_enabled() returns True even after module reloads.
|
||||
|
||||
The _install_fake_tools_package() helper resets and reimports tool modules,
|
||||
so a simple monkeypatch on tool_backend_helpers doesn't survive. We patch
|
||||
the *source* modules that the reimported modules will import from — both
|
||||
hermes_cli.auth and hermes_cli.models — so the function body returns True.
|
||||
"""
|
||||
monkeypatch.setattr("hermes_cli.auth.get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr("hermes_cli.models.check_nous_free_tier", lambda: False)
|
||||
|
||||
|
||||
def _install_fake_tools_package():
|
||||
|
||||
@@ -46,7 +46,10 @@ def _restore_tool_and_agent_modules():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_managed_nous_tools(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
"""Patch the source modules so managed_nous_tools_enabled() returns True
|
||||
even after tool modules are dynamically reloaded."""
|
||||
monkeypatch.setattr("hermes_cli.auth.get_nous_auth_status", lambda: {"logged_in": True})
|
||||
monkeypatch.setattr("hermes_cli.models.check_nous_free_tier", lambda: False)
|
||||
|
||||
|
||||
def _install_fake_tools_package():
|
||||
|
||||
@@ -19,11 +19,10 @@ def test_resolve_managed_tool_gateway_derives_vendor_origin_from_shared_domain()
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
|
||||
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
||||
result = resolve_managed_tool_gateway(
|
||||
"firecrawl",
|
||||
token_reader=lambda: "nous-token",
|
||||
@@ -39,11 +38,10 @@ def test_resolve_managed_tool_gateway_uses_vendor_specific_override():
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
|
||||
"BROWSER_USE_GATEWAY_URL": "http://browser-use-gateway.localhost:3009/",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
||||
result = resolve_managed_tool_gateway(
|
||||
"browser-use",
|
||||
token_reader=lambda: "nous-token",
|
||||
@@ -57,11 +55,10 @@ def test_resolve_managed_tool_gateway_is_inactive_without_nous_token():
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS": "1",
|
||||
"TOOL_GATEWAY_DOMAIN": "nousresearch.com",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
), patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=True):
|
||||
result = resolve_managed_tool_gateway(
|
||||
"firecrawl",
|
||||
token_reader=lambda: None,
|
||||
@@ -70,8 +67,9 @@ def test_resolve_managed_tool_gateway_is_inactive_without_nous_token():
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_resolve_managed_tool_gateway_is_disabled_without_feature_flag():
|
||||
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}, clear=False):
|
||||
def test_resolve_managed_tool_gateway_is_disabled_without_subscription():
|
||||
with patch.dict(os.environ, {"TOOL_GATEWAY_DOMAIN": "nousresearch.com"}, clear=False), \
|
||||
patch.object(managed_tool_gateway, "managed_nous_tools_enabled", return_value=False):
|
||||
result = resolve_managed_tool_gateway(
|
||||
"firecrawl",
|
||||
token_reader=lambda: "nous-token",
|
||||
|
||||
@@ -7,7 +7,6 @@ terminal_tool_module = importlib.import_module("tools.terminal_tool")
|
||||
def _clear_terminal_env(monkeypatch):
|
||||
"""Remove terminal env vars that could affect requirements checks."""
|
||||
keys = [
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
|
||||
"TERMINAL_ENV",
|
||||
"TERMINAL_MODAL_MODE",
|
||||
"TERMINAL_SSH_HOST",
|
||||
@@ -19,6 +18,11 @@ def _clear_terminal_env(monkeypatch):
|
||||
]
|
||||
for key in keys:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
# Default: no Nous subscription — patch both the terminal_tool local
|
||||
# binding and tool_backend_helpers (used by resolve_modal_backend_state).
|
||||
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: False)
|
||||
import tools.tool_backend_helpers as _tbh
|
||||
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: False)
|
||||
|
||||
|
||||
def test_local_terminal_requirements(monkeypatch, caplog):
|
||||
@@ -81,7 +85,9 @@ def test_modal_backend_without_token_or_config_logs_specific_error(monkeypatch,
|
||||
|
||||
def test_modal_backend_with_managed_gateway_does_not_require_direct_creds_or_minisweagent(monkeypatch, tmp_path):
|
||||
_clear_terminal_env(monkeypatch)
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: True)
|
||||
import tools.tool_backend_helpers as _tbh
|
||||
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
||||
@@ -98,7 +104,9 @@ def test_modal_backend_with_managed_gateway_does_not_require_direct_creds_or_min
|
||||
|
||||
def test_modal_backend_auto_mode_prefers_managed_gateway_over_direct_creds(monkeypatch, tmp_path):
|
||||
_clear_terminal_env(monkeypatch)
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: True)
|
||||
import tools.tool_backend_helpers as _tbh
|
||||
monkeypatch.setattr(_tbh, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setenv("TERMINAL_ENV", "modal")
|
||||
monkeypatch.setenv("MODAL_TOKEN_ID", "tok-id")
|
||||
monkeypatch.setenv("MODAL_TOKEN_SECRET", "tok-secret")
|
||||
@@ -147,7 +155,7 @@ def test_modal_backend_managed_mode_does_not_fall_back_to_direct(monkeypatch, ca
|
||||
|
||||
assert ok is False
|
||||
assert any(
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled" in record.getMessage()
|
||||
"paid Nous subscription is required" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
@@ -165,6 +173,6 @@ def test_modal_backend_managed_mode_without_feature_flag_logs_clear_error(monkey
|
||||
|
||||
assert ok is False
|
||||
assert any(
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled" in record.getMessage()
|
||||
"paid Nous subscription is required" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
@@ -28,7 +28,8 @@ class TestTerminalRequirements:
|
||||
assert {"read_file", "write_file", "patch", "search_files"}.issubset(names)
|
||||
|
||||
def test_terminal_and_execute_code_tools_resolve_for_managed_modal(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setattr(terminal_tool_module, "managed_nous_tools_enabled", lambda: True)
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
||||
monkeypatch.delenv("MODAL_TOKEN_ID", raising=False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Unit tests for tools/tool_backend_helpers.py.
|
||||
|
||||
Tests cover:
|
||||
- managed_nous_tools_enabled() feature flag
|
||||
- managed_nous_tools_enabled() subscription-based gate
|
||||
- normalize_browser_cloud_provider() coercion
|
||||
- coerce_modal_mode() / normalize_modal_mode() validation
|
||||
- has_direct_modal_credentials() detection
|
||||
@@ -27,24 +27,51 @@ from tools.tool_backend_helpers import (
|
||||
)
|
||||
|
||||
|
||||
def _raise_import():
|
||||
raise ImportError("simulated missing module")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# managed_nous_tools_enabled
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestManagedNousToolsEnabled:
|
||||
"""Feature flag driven by HERMES_ENABLE_NOUS_MANAGED_TOOLS."""
|
||||
"""Subscription-based gate: True for paid Nous subscribers."""
|
||||
|
||||
def test_disabled_by_default(self, monkeypatch):
|
||||
monkeypatch.delenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", raising=False)
|
||||
def test_disabled_when_not_logged_in(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_nous_auth_status",
|
||||
lambda: {},
|
||||
)
|
||||
assert managed_nous_tools_enabled() is False
|
||||
|
||||
@pytest.mark.parametrize("val", ["1", "true", "True", "yes"])
|
||||
def test_enabled_when_truthy(self, monkeypatch, val):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", val)
|
||||
def test_disabled_for_free_tier(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_nous_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.check_nous_free_tier",
|
||||
lambda: True,
|
||||
)
|
||||
assert managed_nous_tools_enabled() is False
|
||||
|
||||
def test_enabled_for_paid_subscriber(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_nous_auth_status",
|
||||
lambda: {"logged_in": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.check_nous_free_tier",
|
||||
lambda: False,
|
||||
)
|
||||
assert managed_nous_tools_enabled() is True
|
||||
|
||||
@pytest.mark.parametrize("val", ["0", "false", "no", ""])
|
||||
def test_disabled_when_falsy(self, monkeypatch, val):
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", val)
|
||||
def test_returns_false_on_exception(self, monkeypatch):
|
||||
"""Should never crash — returns False on any exception."""
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth.get_nous_auth_status",
|
||||
_raise_import,
|
||||
)
|
||||
assert managed_nous_tools_enabled() is False
|
||||
|
||||
|
||||
@@ -171,10 +198,10 @@ class TestResolveModalBackendState:
|
||||
@staticmethod
|
||||
def _resolve(monkeypatch, mode, *, has_direct, managed_ready, nous_enabled=False):
|
||||
"""Helper to call resolve_modal_backend_state with feature flag control."""
|
||||
if nous_enabled:
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "1")
|
||||
else:
|
||||
monkeypatch.setenv("HERMES_ENABLE_NOUS_MANAGED_TOOLS", "")
|
||||
monkeypatch.setattr(
|
||||
"tools.tool_backend_helpers.managed_nous_tools_enabled",
|
||||
lambda: nous_enabled,
|
||||
)
|
||||
return resolve_modal_backend_state(
|
||||
mode, has_direct=has_direct, managed_ready=managed_ready
|
||||
)
|
||||
|
||||
@@ -26,7 +26,6 @@ class TestFirecrawlClientConfig:
|
||||
tools.web_tools._firecrawl_client = None
|
||||
tools.web_tools._firecrawl_client_config = None
|
||||
for key in (
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL",
|
||||
"FIRECRAWL_GATEWAY_URL",
|
||||
@@ -35,7 +34,15 @@ class TestFirecrawlClientConfig:
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
):
|
||||
os.environ.pop(key, None)
|
||||
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
|
||||
# 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("tools.web_tools.managed_nous_tools_enabled", return_value=True),
|
||||
patch("tools.managed_tool_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."""
|
||||
@@ -43,7 +50,6 @@ class TestFirecrawlClientConfig:
|
||||
tools.web_tools._firecrawl_client = None
|
||||
tools.web_tools._firecrawl_client_config = None
|
||||
for key in (
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
|
||||
"FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL",
|
||||
"FIRECRAWL_GATEWAY_URL",
|
||||
@@ -52,6 +58,8 @@ class TestFirecrawlClientConfig:
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
):
|
||||
os.environ.pop(key, None)
|
||||
for p in self._managed_patchers:
|
||||
p.stop()
|
||||
|
||||
# ── Configuration matrix ─────────────────────────────────────────
|
||||
|
||||
@@ -298,7 +306,6 @@ class TestBackendSelection:
|
||||
"""
|
||||
|
||||
_ENV_KEYS = (
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
|
||||
"EXA_API_KEY",
|
||||
"PARALLEL_API_KEY",
|
||||
"FIRECRAWL_API_KEY",
|
||||
@@ -311,14 +318,20 @@ class TestBackendSelection:
|
||||
)
|
||||
|
||||
def setup_method(self):
|
||||
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
|
||||
for key in self._ENV_KEYS:
|
||||
if key != "HERMES_ENABLE_NOUS_MANAGED_TOOLS":
|
||||
os.environ.pop(key, None)
|
||||
os.environ.pop(key, None)
|
||||
self._managed_patchers = [
|
||||
patch("tools.web_tools.managed_nous_tools_enabled", return_value=True),
|
||||
patch("tools.managed_tool_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) ───────────
|
||||
|
||||
@@ -523,7 +536,6 @@ class TestCheckWebApiKey:
|
||||
"""Test suite for check_web_api_key() unified availability check."""
|
||||
|
||||
_ENV_KEYS = (
|
||||
"HERMES_ENABLE_NOUS_MANAGED_TOOLS",
|
||||
"EXA_API_KEY",
|
||||
"PARALLEL_API_KEY",
|
||||
"FIRECRAWL_API_KEY",
|
||||
@@ -536,14 +548,20 @@ class TestCheckWebApiKey:
|
||||
)
|
||||
|
||||
def setup_method(self):
|
||||
os.environ["HERMES_ENABLE_NOUS_MANAGED_TOOLS"] = "1"
|
||||
for key in self._ENV_KEYS:
|
||||
if key != "HERMES_ENABLE_NOUS_MANAGED_TOOLS":
|
||||
os.environ.pop(key, None)
|
||||
os.environ.pop(key, None)
|
||||
self._managed_patchers = [
|
||||
patch("tools.web_tools.managed_nous_tools_enabled", return_value=True),
|
||||
patch("tools.managed_tool_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"}):
|
||||
|
||||
Reference in New Issue
Block a user