feat: add hermes backend support
- Add myhermes entrypoint and myagents hermes subcommand - Hermes sessions read from global SQLite store (~/.hermes/state.db) - Default args: --yolo - Update README, AGENTS.md, Makefile, pyproject.toml - Add tests for hermes subcommand, passthrough, and session listing
This commit is contained in:
+135
-1
@@ -1,6 +1,7 @@
|
||||
"""Tests for myagents CLI."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -14,13 +15,14 @@ class TestMyagentsHelp:
|
||||
"""Tests for top-level myagents command."""
|
||||
|
||||
def test_help_shows_agent_subcommands(self) -> None:
|
||||
"""--help should list claude, kimi and codex subcommands."""
|
||||
"""--help should list claude, kimi, codex and hermes subcommands."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "claude" in result.output
|
||||
assert "kimi" in result.output
|
||||
assert "codex" in result.output
|
||||
assert "hermes" in result.output
|
||||
assert "update" in result.output
|
||||
assert "upgrade" in result.output
|
||||
|
||||
@@ -400,3 +402,135 @@ class TestCodexListSessions:
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "019aeb67-0620-71c1-8cbb-756bc8845c6e" in result.output
|
||||
assert "hello codex" in result.output
|
||||
|
||||
|
||||
class TestHermesSubcommand:
|
||||
"""Tests for ``myagents hermes``."""
|
||||
|
||||
def test_help_shows_options(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["hermes", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--cwd" in result.output
|
||||
assert "--list" in result.output
|
||||
|
||||
def test_runs_hermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["hermes"])
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/hermes", "--yolo"]
|
||||
|
||||
def test_missing_binary_error(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.shutil.which", return_value=None):
|
||||
result = runner.invoke(cli, ["hermes"])
|
||||
assert result.exit_code == 127
|
||||
assert "not found" in result.output.lower()
|
||||
|
||||
def test_cwd_option_passed(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
test_dir = tmp_path / "test_cwd"
|
||||
test_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["hermes", "--cwd", str(test_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
|
||||
|
||||
|
||||
class TestHermesPassthrough:
|
||||
"""Unknown leading flags forward to hermes instead of erroring."""
|
||||
|
||||
def _invoke(self, args: list[str], tmp_path: Path) -> list[str]:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--cwd", str(tmp_path), *args]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_run.assert_called_once()
|
||||
return mock_run.call_args[0][0]
|
||||
|
||||
def test_resume_with_session_id(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--resume", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/hermes", "--yolo", "--resume", "abc123"]
|
||||
|
||||
def test_continue_flag_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--continue"], tmp_path)
|
||||
assert cmd[-1] == "--continue"
|
||||
|
||||
|
||||
class TestHermesListSessions:
|
||||
"""``myagents hermes --list`` reads the global SQLite session store."""
|
||||
|
||||
def _patch_state_db(self, db_path: Path):
|
||||
return patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"hermes": {
|
||||
**myagents.launcher._BACKENDS["hermes"],
|
||||
"state_db": lambda: db_path,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_missing_database(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with self._patch_state_db(tmp_path / "missing.db"):
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--cwd", str(tmp_path), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "no sessions" in result.output.lower()
|
||||
|
||||
def test_list_shows_session_id_and_snippet(self, tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "state.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute(
|
||||
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT NOT NULL,"
|
||||
" started_at REAL NOT NULL, title TEXT)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
" session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT,"
|
||||
" timestamp REAL NOT NULL)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO sessions (id, source, started_at)"
|
||||
" VALUES ('sess-abc123', 'cli', 1767220800)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, timestamp)"
|
||||
" VALUES ('sess-abc123', 'user', 'hello hermes', 1767220801)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
runner = CliRunner()
|
||||
with self._patch_state_db(db_path):
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--cwd", str(tmp_path), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "sess-abc123" in result.output
|
||||
assert "hello hermes" in result.output
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for standalone myclaude / mykimi / mycodex entrypoints."""
|
||||
"""Tests for standalone myclaude / mykimi / mycodex / myhermes entrypoints."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
from myagents.entrypoints import claude_cli, codex_cli, hermes_cli, kimi_cli
|
||||
|
||||
|
||||
class TestMyclaudeEntrypoint:
|
||||
@@ -105,3 +105,37 @@ class TestMycodexEntrypoint:
|
||||
result = runner.invoke(codex_cli, ["--cwd", str(tmp_path), "-r", "abc123"])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex", "--dangerously-bypass-approvals-and-sandbox", "resume", "abc123"]
|
||||
|
||||
|
||||
class TestMyhermesEntrypoint:
|
||||
"""``myhermes`` standalone entrypoint."""
|
||||
|
||||
def test_runs_hermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/hermes"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(hermes_cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/hermes", "--yolo"]
|
||||
|
||||
def test_version_shows_myhermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(hermes_cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "myhermes" in result.output
|
||||
|
||||
def test_passthrough(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/hermes"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
hermes_cli, ["--cwd", str(tmp_path), "--resume", "abc123"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0][-2:] == ["--resume", "abc123"]
|
||||
|
||||
Reference in New Issue
Block a user