feat(integrations): add Hermes provider with shared memory utilities
This commit is contained in:
@@ -15,6 +15,7 @@ from build.build import ( # noqa: E402
|
||||
bundle_drift,
|
||||
render_template,
|
||||
replace_output,
|
||||
sync_generated,
|
||||
)
|
||||
from build.validate import validate_bundle # noqa: E402
|
||||
|
||||
@@ -113,6 +114,7 @@ def test_native_control_skills_select_the_host_store(host: str, tmp_path: Path)
|
||||
("codex", "native"),
|
||||
("kimi", "native"),
|
||||
("antigravity", "native"),
|
||||
("hermes", "native"),
|
||||
],
|
||||
)
|
||||
def test_installable_plugin_directories_are_current(host: str, kind: str) -> None:
|
||||
@@ -133,3 +135,91 @@ def test_marketplaces_keep_public_names_and_reference_real_plugins() -> None:
|
||||
assert [plugin["name"] for plugin in codex_marketplace["plugins"]] == ["mem0"]
|
||||
codex = codex_marketplace["plugins"][0]
|
||||
assert codex["source"]["path"] == "./integrations/codex-plugin"
|
||||
|
||||
|
||||
def test_native_bundle_can_select_runtime_without_skills(tmp_path: Path, monkeypatch) -> None:
|
||||
from build import build as builder
|
||||
|
||||
source = tmp_path / "plugin"
|
||||
source.mkdir()
|
||||
(source / "__init__.py").write_text("# Native plugin adapter\n", encoding="utf-8")
|
||||
(source / "plugin-build.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"native": {
|
||||
"pluginRoot": "${PLUGIN_ROOT}",
|
||||
"pythonFiles": ["message_utils.py"],
|
||||
"skills": False,
|
||||
"files": {"__init__.py": "__init__.py"},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setitem(builder.NATIVE_PLUGINS, "test-host", source)
|
||||
stale_skill = source / "skills" / "remember" / "SKILL.md"
|
||||
stale_skill.parent.mkdir(parents=True)
|
||||
stale_skill.write_text("stale generated skill", encoding="utf-8")
|
||||
|
||||
root = build("test-host", "native", tmp_path / "output")
|
||||
|
||||
assert {path.name for path in (root / "core").iterdir()} == {"message_utils.py"}
|
||||
assert not (root / "skills").exists()
|
||||
assert (root / "__init__.py").read_text(encoding="utf-8") == "# Native plugin adapter\n"
|
||||
sync_generated("test-host", "native")
|
||||
assert bundle_drift("test-host", "native") == []
|
||||
assert not (source / "skills").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("files", ["message_utils.py", ["../README.md"], ["/tmp/source.py"], [7], ["missing.py"]])
|
||||
def test_native_runtime_selection_rejects_invalid_sources(files, tmp_path: Path) -> None:
|
||||
from build.build import _build_native
|
||||
|
||||
with pytest.raises(ValueError, match="pythonFiles|native source file"):
|
||||
_build_native(
|
||||
"test",
|
||||
tmp_path,
|
||||
tmp_path,
|
||||
{
|
||||
"native": {
|
||||
"pluginRoot": "${PLUGIN_ROOT}",
|
||||
"pythonFiles": files,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_native_runtime_selection_rejects_escaping_symlink(tmp_path: Path, monkeypatch) -> None:
|
||||
from build import build as builder
|
||||
|
||||
source = tmp_path / "shared" / "python"
|
||||
source.mkdir(parents=True)
|
||||
secret = tmp_path / "outside.py"
|
||||
secret.write_text("secret", encoding="utf-8")
|
||||
(source / "message_utils.py").symlink_to(secret)
|
||||
monkeypatch.setattr(builder, "CORE_ROOT", source.parent)
|
||||
staged = tmp_path / "bundle"
|
||||
staged.mkdir()
|
||||
|
||||
with pytest.raises(ValueError, match="inside their roots"):
|
||||
builder._build_native(
|
||||
"test",
|
||||
tmp_path,
|
||||
staged,
|
||||
{
|
||||
"native": {
|
||||
"pluginRoot": "${PLUGIN_ROOT}",
|
||||
"pythonFiles": ["message_utils.py"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_hermes_bundle_uses_only_host_independent_runtime(tmp_path: Path) -> None:
|
||||
root = build("hermes", "native", tmp_path / "hermes")
|
||||
|
||||
assert {path.name for path in (root / "core").iterdir()} == {"message_utils.py"}
|
||||
assert (root / "__init__.py").is_file()
|
||||
assert (root / "plugin.yaml").is_file()
|
||||
assert not (root / "skills").exists()
|
||||
assert not (root / "agents").exists()
|
||||
|
||||
@@ -10,10 +10,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from conformance import run as conformance_run # noqa: E402
|
||||
from conformance.run import _command_check # noqa: E402
|
||||
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNNER = PLUGIN_ROOT / "conformance" / "run.py"
|
||||
PYTHON_HOSTS = {"claude-code", "cursor", "codex", "kimi", "antigravity"}
|
||||
PYTHON_HOSTS = {"claude-code", "cursor", "codex", "kimi", "antigravity", "hermes"}
|
||||
|
||||
|
||||
def test_python_bundle_conformance_builds_every_host(tmp_path: Path) -> None:
|
||||
@@ -78,6 +77,12 @@ def test_conformance_plan_covers_every_runtime(tmp_path: Path) -> None:
|
||||
"deepseek",
|
||||
}
|
||||
assert all(entry["status"] == "planned" for entry in payload["checks"])
|
||||
python_commands = [entry["command"] for entry in payload["checks"] if entry["group"] == "python-tests"]
|
||||
assert any(
|
||||
"integrations/hermes-plugin/tests" in command
|
||||
and "--confcutdir=integrations/hermes-plugin/tests" in command
|
||||
for command in python_commands
|
||||
)
|
||||
assert {
|
||||
entry["group"]
|
||||
for entry in payload["checks"]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CORE = Path(__file__).resolve().parents[1] / "python"
|
||||
|
||||
|
||||
def test_message_helpers_are_standalone_and_keep_legacy_exports() -> None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import message_utils
|
||||
assert "memory_core" not in sys.modules
|
||||
assert "telemetry" not in sys.modules
|
||||
text = "prefix " + "日本語🙂" * 250 + ' {"password": "secret value"}'
|
||||
redacted = message_utils.redact(text)
|
||||
assert "secret value" not in redacted
|
||||
messages = [{"role": "user", "content": redacted}]
|
||||
batches = message_utils.extraction_message_batches(messages, max_tokens=100)
|
||||
assert len(batches) > 1
|
||||
assert all(message_utils._message_tokens(batch) <= 100 for batch in batches)
|
||||
assert "".join(m["content"] for batch in batches for m in batch) == redacted
|
||||
import memory_core
|
||||
for name in ("redact", "SECRET_PATTERNS", "extraction_message_batches", "_message_tokens"):
|
||||
assert getattr(memory_core, name) is getattr(message_utils, name)
|
||||
""",
|
||||
],
|
||||
cwd=CORE,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
Reference in New Issue
Block a user