Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

This commit is contained in:
Brooklyn Nicholson
2026-05-04 16:08:48 -05:00
58 changed files with 6334 additions and 277 deletions
+61
View File
@@ -2,6 +2,7 @@
import os
import pytest
import subprocess
from pathlib import Path
from unittest.mock import MagicMock
@@ -388,6 +389,66 @@ class TestSearchPathValidation:
assert "search failed" in result.error.lower() or "Search error" in result.error
class TestSearchFilesFallbackHiddenPaths:
def _make_env(self):
env = MagicMock()
env.cwd = "/"
def execute(command, **kwargs):
completed = subprocess.run(
command,
shell=True,
text=True,
capture_output=True,
)
return {
"output": completed.stdout,
"returncode": completed.returncode,
}
env.execute = execute
return env
def test_hidden_root_with_hidden_ancestor_includes_files(self, tmp_path, monkeypatch):
"""Fallback find should include visible files when path is inside hidden root."""
root = tmp_path / ".hermes" / "logs"
root.mkdir(parents=True)
visible_file = root / "agent.log"
hidden_dir_file = root / ".hidden" / "secret.log"
nested_hidden_file = root / "nested" / ".secret.log"
visible_nested_file = root / "nested" / "visible.log"
for p in [visible_file, nested_hidden_file, visible_nested_file, hidden_dir_file]:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("x")
ops = ShellFileOperations(self._make_env())
monkeypatch.setattr(ops, "_has_command", lambda command: command == "find")
result = ops._search_files("*.log", str(root), limit=50, offset=0)
assert result.error is None
assert set(result.files) == {str(visible_file), str(visible_nested_file)}
def test_normal_root_still_excludes_hidden_descendants(self, tmp_path, monkeypatch):
"""Fallback find should still exclude hidden descendant paths for normal roots."""
root = tmp_path / "repo"
root.mkdir()
visible_file = root / "agent.log"
visible_nested_file = root / "nested" / "visible.log"
hidden_dir_file = root / ".hidden" / "secret.log"
for p in [visible_file, visible_nested_file, hidden_dir_file]:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("x")
ops = ShellFileOperations(self._make_env())
monkeypatch.setattr(ops, "_has_command", lambda command: command == "find")
result = ops._search_files("*.log", str(root), limit=50, offset=0)
assert result.error is None
assert set(result.files) == {str(visible_file), str(visible_nested_file)}
class TestShellFileOpsWriteDenied:
def test_write_file_denied_path(self, file_ops):
result = file_ops.write_file("~/.ssh/authorized_keys", "evil key")
+65 -1
View File
@@ -8,7 +8,7 @@ Covers:
import pytest
from unittest.mock import MagicMock, patch
from tools.file_operations import ShellFileOperations
from tools.file_operations import ShellFileOperations, _parse_search_context_line
# =========================================================================
@@ -204,3 +204,67 @@ class TestPaginationBounds:
rg_commands = [cmd for cmd in commands if cmd.startswith("rg --files")]
assert rg_commands
assert "| head -n 1" in rg_commands[0]
# =========================================================================
# Search context parsing
# =========================================================================
class TestSearchContextParsing:
def test_parse_search_context_line_prefers_rightmost_numeric_separator(self):
parsed = _parse_search_context_line("dir/file-12-name.py-8-context here")
assert parsed == ("dir/file-12-name.py", 8, "context here")
def test_search_with_rg_context_handles_filename_with_dash_digits(self):
env = MagicMock()
env.cwd = "/tmp"
ops = ShellFileOperations(env)
with patch.object(ops, "_exec") as mock_exec:
mock_exec.return_value = MagicMock(
exit_code=0,
stdout="dir/file-12-name.py-8-context here\n",
)
result = ops._search_with_rg(
"needle",
path=".",
file_glob=None,
limit=10,
offset=0,
output_mode="content",
context=1,
)
assert result.error is None
assert result.total_count == 1
assert result.matches[0].path == "dir/file-12-name.py"
assert result.matches[0].line_number == 8
assert result.matches[0].content == "context here"
def test_search_with_grep_context_handles_filename_with_dash_digits(self):
env = MagicMock()
env.cwd = "/tmp"
ops = ShellFileOperations(env)
with patch.object(ops, "_exec") as mock_exec:
mock_exec.return_value = MagicMock(
exit_code=0,
stdout="dir/file-12-name.py-8-context here\n",
)
result = ops._search_with_grep(
"needle",
path=".",
file_glob=None,
limit=10,
offset=0,
output_mode="content",
context=1,
)
assert result.error is None
assert result.total_count == 1
assert result.matches[0].path == "dir/file-12-name.py"
assert result.matches[0].line_number == 8
assert result.matches[0].content == "context here"
+20 -19
View File
@@ -110,7 +110,7 @@ class TestOpenaiTtsSpeed:
# ---------------------------------------------------------------------------
# MiniMax TTS speed (global fallback wired)
# MiniMax TTS (new API: raw audio, no speed/voice_setting)
# ---------------------------------------------------------------------------
class TestMinimaxTtsSpeed:
@@ -118,28 +118,29 @@ class TestMinimaxTtsSpeed:
monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {"audio": "deadbeef"},
"base_resp": {"status_code": 0, "status_msg": "success"},
"extra_info": {"audio_size": 8},
}
mock_response.headers = {"Content-Type": "audio/mpeg"}
mock_response.content = b"\x00\x01\x02\x03"
# requests is imported locally inside _generate_minimax_tts
with patch("requests.post", return_value=mock_response) as mock_post:
from tools.tts_tool import _generate_minimax_tts
_generate_minimax_tts("Hello", str(tmp_path / "out.mp3"), tts_config)
return mock_post
output = _generate_minimax_tts("Hello", str(tmp_path / "out.mp3"), tts_config)
return mock_post, output
def test_global_speed_fallback(self, tmp_path, monkeypatch):
"""Global tts.speed used when minimax.speed not set."""
mock_post = self._run({"speed": 1.5}, tmp_path, monkeypatch)
def test_simple_payload(self, tmp_path, monkeypatch):
"""New API uses flat payload with model, text, voice_id."""
mock_post, _ = self._run({}, tmp_path, monkeypatch)
payload = mock_post.call_args[1]["json"]
assert payload["voice_setting"]["speed"] == 1.5
assert "model" in payload
assert "text" in payload
assert "voice_id" in payload
assert "voice_setting" not in payload
assert "audio_setting" not in payload
assert "stream" not in payload
def test_provider_speed_overrides_global(self, tmp_path, monkeypatch):
"""tts.minimax.speed takes precedence over tts.speed."""
mock_post = self._run(
{"speed": 1.5, "minimax": {"speed": 2.0}}, tmp_path, monkeypatch
)
payload = mock_post.call_args[1]["json"]
assert payload["voice_setting"]["speed"] == 2.0
def test_writes_raw_audio(self, tmp_path, monkeypatch):
"""New API returns raw bytes written directly to file."""
_, output = self._run({}, tmp_path, monkeypatch)
assert output == str(tmp_path / "out.mp3")
with open(output, "rb") as f:
assert f.read() == b"\x00\x01\x02\x03"