Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor

This commit is contained in:
Brooklyn Nicholson
2026-04-11 17:15:41 -05:00
93 changed files with 3531 additions and 1330 deletions

View File

@@ -260,7 +260,7 @@ class TestWaitForGatewayExit:
def test_kill_gateway_processes_force_uses_helper(self, monkeypatch):
calls = []
monkeypatch.setattr(gateway, "find_gateway_pids", lambda exclude_pids=None: [11, 22])
monkeypatch.setattr(gateway, "find_gateway_pids", lambda exclude_pids=None, all_profiles=False: [11, 22])
monkeypatch.setattr(gateway, "terminate_pid", lambda pid, force=False: calls.append((pid, force)))
killed = gateway.kill_gateway_processes(force=True)

View File

@@ -1,6 +1,7 @@
"""Tests for gateway service management helpers."""
import os
import pwd
from pathlib import Path
from types import SimpleNamespace
@@ -129,7 +130,7 @@ class TestGatewayStopCleanup:
monkeypatch.setattr(
gateway_cli,
"kill_gateway_processes",
lambda force=False: kill_calls.append(force) or 2,
lambda force=False, all_profiles=False: kill_calls.append(force) or 2,
)
gateway_cli.gateway_command(SimpleNamespace(gateway_command="stop"))
@@ -155,7 +156,7 @@ class TestGatewayStopCleanup:
monkeypatch.setattr(
gateway_cli,
"kill_gateway_processes",
lambda force=False: kill_calls.append(force) or 2,
lambda force=False, all_profiles=False: kill_calls.append(force) or 2,
)
gateway_cli.gateway_command(SimpleNamespace(gateway_command="stop", **{"all": True}))
@@ -924,6 +925,23 @@ class TestProfileArg:
assert "<string>--profile</string>" in plist
assert "<string>mybot</string>" in plist
def test_launchd_plist_path_uses_real_user_home_not_profile_home(self, tmp_path, monkeypatch):
profile_dir = tmp_path / ".hermes" / "profiles" / "orcha"
profile_dir.mkdir(parents=True)
machine_home = tmp_path / "machine-home"
machine_home.mkdir()
profile_home = profile_dir / "home"
profile_home.mkdir()
monkeypatch.setattr(Path, "home", lambda: profile_home)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
monkeypatch.setattr(pwd, "getpwuid", lambda uid: SimpleNamespace(pw_dir=str(machine_home)))
plist_path = gateway_cli.get_launchd_plist_path()
assert plist_path == machine_home / "Library" / "LaunchAgents" / "ai.hermes.gateway-orcha.plist"
class TestRemapPathForUser:
"""Unit tests for _remap_path_for_user()."""

View File

@@ -1214,3 +1214,115 @@ def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):
resolved = rp.resolve_runtime_provider(requested="openrouter")
assert resolved["provider"] == "openrouter"
# ------------------------------------------------------------------
# fix #7828 — custom_providers model field must propagate to runtime
# ------------------------------------------------------------------
def test_get_named_custom_provider_includes_model(monkeypatch):
"""_get_named_custom_provider should include the model field from config."""
monkeypatch.setattr(rp, "load_config", lambda: {
"custom_providers": [{
"name": "my-dashscope",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key": "test-key",
"api_mode": "chat_completions",
"model": "qwen3.6-plus",
}],
})
result = rp._get_named_custom_provider("my-dashscope")
assert result is not None
assert result["model"] == "qwen3.6-plus"
def test_get_named_custom_provider_excludes_empty_model(monkeypatch):
"""Empty or whitespace-only model field should not appear in result."""
for model_val in ["", " ", None]:
entry = {
"name": "test-ep",
"base_url": "https://example.com/v1",
"api_key": "key",
}
if model_val is not None:
entry["model"] = model_val
monkeypatch.setattr(rp, "load_config", lambda e=entry: {
"custom_providers": [e],
})
result = rp._get_named_custom_provider("test-ep")
assert result is not None
assert "model" not in result, (
f"model field {model_val!r} should not be included in result"
)
def test_named_custom_runtime_propagates_model_direct_path(monkeypatch):
"""Model should propagate through the direct (non-pool) resolution path."""
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
monkeypatch.setattr(
rp, "_get_named_custom_provider",
lambda p: {
"name": "my-server",
"base_url": "http://localhost:8000/v1",
"api_key": "test-key",
"model": "qwen3.6-plus",
},
)
# Ensure pool doesn't intercept
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
resolved = rp.resolve_runtime_provider(requested="my-server")
assert resolved["model"] == "qwen3.6-plus"
assert resolved["provider"] == "custom"
def test_named_custom_runtime_propagates_model_pool_path(monkeypatch):
"""Model should propagate even when credential pool handles credentials."""
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
monkeypatch.setattr(
rp, "_get_named_custom_provider",
lambda p: {
"name": "my-server",
"base_url": "http://localhost:8000/v1",
"api_key": "test-key",
"model": "qwen3.6-plus",
},
)
# Pool returns a result (intercepting the normal path)
monkeypatch.setattr(
rp, "_try_resolve_from_custom_pool",
lambda *a, **k: {
"provider": "custom",
"api_mode": "chat_completions",
"base_url": "http://localhost:8000/v1",
"api_key": "pool-key",
"source": "pool:custom:my-server",
},
)
resolved = rp.resolve_runtime_provider(requested="my-server")
assert resolved["model"] == "qwen3.6-plus", (
"model must be injected into pool result"
)
assert resolved["api_key"] == "pool-key", "pool credentials should be used"
def test_named_custom_runtime_no_model_when_absent(monkeypatch):
"""When custom_providers entry has no model field, runtime should not either."""
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server")
monkeypatch.setattr(
rp, "_get_named_custom_provider",
lambda p: {
"name": "my-server",
"base_url": "http://localhost:8000/v1",
"api_key": "test-key",
},
)
monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None)
resolved = rp.resolve_runtime_provider(requested="my-server")
assert "model" not in resolved

View File

@@ -191,6 +191,19 @@ class TestLaunchdPlistPath:
raise AssertionError("PATH key not found in plist")
class TestLaunchdPlistCurrentness:
def test_launchd_plist_is_current_ignores_path_drift(self, tmp_path, monkeypatch):
plist_path = tmp_path / "ai.hermes.gateway.plist"
monkeypatch.setattr(gateway_cli, "get_launchd_plist_path", lambda: plist_path)
monkeypatch.setenv("PATH", "/custom/bin:/usr/bin:/bin")
plist_path.write_text(gateway_cli.generate_launchd_plist(), encoding="utf-8")
monkeypatch.setenv("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin")
assert gateway_cli.launchd_plist_is_current() is True
# ---------------------------------------------------------------------------
# cmd_update — macOS launchd detection
# ---------------------------------------------------------------------------
@@ -536,7 +549,7 @@ class TestServicePidExclusion:
gateway_cli, "_get_service_pids", return_value={SERVICE_PID}
), patch.object(
gateway_cli, "find_gateway_pids",
side_effect=lambda exclude_pids=None: (
side_effect=lambda exclude_pids=None, all_profiles=False: (
[SERVICE_PID] if not exclude_pids else
[p for p in [SERVICE_PID] if p not in exclude_pids]
),
@@ -579,7 +592,7 @@ class TestServicePidExclusion:
gateway_cli, "_get_service_pids", return_value={SERVICE_PID}
), patch.object(
gateway_cli, "find_gateway_pids",
side_effect=lambda exclude_pids=None: (
side_effect=lambda exclude_pids=None, all_profiles=False: (
[SERVICE_PID] if not exclude_pids else
[p for p in [SERVICE_PID] if p not in exclude_pids]
),
@@ -618,7 +631,7 @@ class TestServicePidExclusion:
launchctl_loaded=True,
)
def fake_find(exclude_pids=None):
def fake_find(exclude_pids=None, all_profiles=False):
_exclude = exclude_pids or set()
return [p for p in [SERVICE_PID, MANUAL_PID] if p not in _exclude]
@@ -760,3 +773,28 @@ class TestFindGatewayPidsExclude:
pids = gateway_cli.find_gateway_pids()
assert 100 in pids
assert 200 in pids
def test_filters_to_current_profile(self, monkeypatch, tmp_path):
profile_dir = tmp_path / ".hermes" / "profiles" / "orcha"
profile_dir.mkdir(parents=True)
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
def fake_run(cmd, **kwargs):
return subprocess.CompletedProcess(
cmd, 0,
stdout=(
"100 /Users/dgrieco/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main --profile orcha gateway run --replace\n"
"200 /Users/dgrieco/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main --profile other gateway run --replace\n"
),
stderr="",
)
monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run)
monkeypatch.setattr("os.getpid", lambda: 999)
monkeypatch.setattr(gateway_cli, "_get_service_pids", lambda: set())
monkeypatch.setattr(gateway_cli, "_profile_arg", lambda hermes_home=None: "--profile orcha")
pids = gateway_cli.find_gateway_pids()
assert pids == [100]