mirror of
https://github.com/obra/superpowers.git
synced 2026-07-26 04:04:03 +08:00
feat(hermes): Hermes Agent harness support, rebased to a Hermes-only diff
Rebase of PR #1922 onto current dev: the ~14 files of v6.1.0-era codex/release drift are dropped, the porting-guide edits (stale against the post-prune rewrite, no Hermes content) are dropped, and the Hermes surface is kept intact: .hermes-plugin/ (on_session_start bootstrap injection), tests/hermes/ (20 tests, passing), docs/README.hermes.md, references/hermes-tools.md, the Platform Adaptation row, README section, and Python ignores. Known open items from review, unchanged by this rebase: the injection mechanism uses ctx.inject_message from on_session_start, which the official plugin guide does not document (pre_llm_call returning {"context": ...} is the sanctioned path), skills are not registered via ctx.register_skill, and the acceptance transcript predates the fix. Co-authored-by: kumarabd <kumarabd@users.noreply.github.com>
This commit is contained in:
0
tests/hermes/__init__.py
Normal file
0
tests/hermes/__init__.py
Normal file
19
tests/hermes/conftest.py
Normal file
19
tests/hermes/conftest.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ctx():
|
||||
ctx = MagicMock()
|
||||
ctx._hooks = {}
|
||||
ctx._injected = []
|
||||
|
||||
def register_hook(event, fn):
|
||||
ctx._hooks[event] = fn
|
||||
|
||||
def inject_message(content, role="user"):
|
||||
ctx._injected.append({"content": content, "role": role})
|
||||
|
||||
ctx.register_hook.side_effect = register_hook
|
||||
ctx.inject_message.side_effect = inject_message
|
||||
return ctx
|
||||
81
tests/hermes/test_bootstrap.py
Normal file
81
tests/hermes/test_bootstrap.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "../../.hermes-plugin")
|
||||
))
|
||||
|
||||
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
||||
|
||||
|
||||
def _load():
|
||||
if "__init__" in sys.modules:
|
||||
del sys.modules["__init__"]
|
||||
return importlib.import_module("__init__")
|
||||
|
||||
|
||||
class TestStripFrontmatter:
|
||||
def test_strips_yaml_block(self):
|
||||
m = _load()
|
||||
content = "---\nname: foo\ndescription: bar\n---\n# Body\nContent here"
|
||||
assert m._strip_frontmatter(content) == "# Body\nContent here"
|
||||
|
||||
def test_no_frontmatter_returns_trimmed_content(self):
|
||||
m = _load()
|
||||
content = "# No frontmatter\nJust content"
|
||||
assert m._strip_frontmatter(content) == "# No frontmatter\nJust content"
|
||||
|
||||
def test_strips_surrounding_whitespace_from_body(self):
|
||||
m = _load()
|
||||
content = "---\nname: foo\n---\n\n\n# Body\n\n"
|
||||
assert m._strip_frontmatter(content) == "# Body"
|
||||
|
||||
|
||||
class TestGetBootstrap:
|
||||
def test_returns_none_when_skill_file_missing(self, tmp_path):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
m._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||
assert m._get_bootstrap() is None
|
||||
|
||||
def test_caches_false_on_missing_file(self, tmp_path):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
m._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||
m._get_bootstrap()
|
||||
assert m._bootstrap_cache is False
|
||||
|
||||
def test_returns_string_with_real_skill(self):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
result = m._get_bootstrap()
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_same_object_returned_on_second_call(self):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
r1 = m._get_bootstrap()
|
||||
r2 = m._get_bootstrap()
|
||||
assert r1 is r2
|
||||
|
||||
def test_contains_marker(self):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
result = m._get_bootstrap()
|
||||
assert BOOTSTRAP_MARKER in result
|
||||
|
||||
def test_contains_extremely_important_wrapper(self):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
result = m._get_bootstrap()
|
||||
assert result.startswith("<EXTREMELY_IMPORTANT>")
|
||||
assert result.rstrip().endswith("</EXTREMELY_IMPORTANT>")
|
||||
|
||||
def test_frontmatter_absent_from_output(self):
|
||||
m = _load()
|
||||
m._bootstrap_cache = None
|
||||
result = m._get_bootstrap()
|
||||
assert "---\nname:" not in result
|
||||
105
tests/hermes/test_plugin.py
Normal file
105
tests/hermes/test_plugin.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import pytest
|
||||
|
||||
# Point at the plugin directory
|
||||
_PLUGIN_DIR = os.path.join(os.path.dirname(__file__), "../../.hermes-plugin")
|
||||
sys.path.insert(0, os.path.abspath(_PLUGIN_DIR))
|
||||
|
||||
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
||||
|
||||
|
||||
def _load_plugin():
|
||||
"""Re-import plugin module fresh (clears module-level cache)."""
|
||||
if "__init__" in sys.modules:
|
||||
del sys.modules["__init__"]
|
||||
return importlib.import_module("__init__")
|
||||
|
||||
|
||||
class TestPluginRegistration:
|
||||
def test_register_attaches_session_start_hook(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
mock_ctx.register_hook.assert_called_once()
|
||||
event_name = mock_ctx.register_hook.call_args[0][0]
|
||||
assert event_name == "on_session_start"
|
||||
|
||||
|
||||
class TestBootstrapInjection:
|
||||
def test_first_session_start_injects_bootstrap(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
assert len(mock_ctx._injected) == 1
|
||||
assert BOOTSTRAP_MARKER in mock_ctx._injected[0]["content"]
|
||||
|
||||
def test_injection_uses_user_role(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
assert mock_ctx._injected[0]["role"] == "user"
|
||||
|
||||
def test_dedup_skips_on_same_session_id(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
assert len(mock_ctx._injected) == 1
|
||||
|
||||
def test_reinjects_on_new_session_id(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
handler(session_id="sess-2", model="test-model", platform="test")
|
||||
assert len(mock_ctx._injected) == 2
|
||||
|
||||
def test_missing_skill_file_skips_silently(self, mock_ctx, tmp_path):
|
||||
plugin = _load_plugin()
|
||||
plugin._bootstrap_cache = None
|
||||
plugin._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
assert len(mock_ctx._injected) == 0
|
||||
|
||||
def test_cache_populated_after_first_call(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin._bootstrap_cache = None
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
assert plugin._bootstrap_cache is not None
|
||||
assert plugin._bootstrap_cache is not False
|
||||
|
||||
|
||||
class TestBootstrapContent:
|
||||
def test_contains_extremely_important_tags(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
content = mock_ctx._injected[0]["content"]
|
||||
assert "<EXTREMELY_IMPORTANT>" in content
|
||||
assert "</EXTREMELY_IMPORTANT>" in content
|
||||
|
||||
def test_frontmatter_stripped(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
content = mock_ctx._injected[0]["content"]
|
||||
assert "---\nname:" not in content
|
||||
|
||||
def test_tool_mapping_present(self, mock_ctx):
|
||||
plugin = _load_plugin()
|
||||
plugin.register(mock_ctx)
|
||||
handler = mock_ctx._hooks["on_session_start"]
|
||||
handler(session_id="sess-1", model="test-model", platform="test")
|
||||
content = mock_ctx._injected[0]["content"]
|
||||
assert "Hermes tool mapping" in content
|
||||
assert "read_file" in content
|
||||
Reference in New Issue
Block a user