mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
feat: add ACP registry metadata for Zed
This commit is contained in:
parent
e8b9f5ff9a
commit
4c94396206
17 changed files with 683 additions and 75 deletions
|
|
@ -1,6 +1,11 @@
|
|||
"""Tests for acp_adapter.auth — provider detection."""
|
||||
|
||||
from acp_adapter.auth import has_provider, detect_provider
|
||||
from acp_adapter.auth import (
|
||||
TERMINAL_SETUP_AUTH_METHOD_ID,
|
||||
build_auth_methods,
|
||||
has_provider,
|
||||
detect_provider,
|
||||
)
|
||||
|
||||
|
||||
class TestHasProvider:
|
||||
|
|
@ -54,3 +59,44 @@ class TestDetectProvider:
|
|||
|
||||
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _boom)
|
||||
assert detect_provider() is None
|
||||
|
||||
def test_detect_provider_strips_and_lowercases_provider(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.runtime_provider.resolve_runtime_provider",
|
||||
lambda: {"provider": " OpenRouter ", "api_key": " sk-or-test "},
|
||||
)
|
||||
assert detect_provider() == "openrouter"
|
||||
|
||||
|
||||
class TestBuildAuthMethods:
|
||||
def test_build_auth_methods_returns_provider_and_terminal_when_configured(self, monkeypatch):
|
||||
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: "openrouter")
|
||||
|
||||
methods = build_auth_methods()
|
||||
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in methods]
|
||||
|
||||
assert payloads[0]["id"] == "openrouter"
|
||||
assert payloads[0]["name"] == "openrouter runtime credentials"
|
||||
assert any(payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID for payload in payloads)
|
||||
terminal = next(payload for payload in payloads if payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID)
|
||||
assert terminal["type"] == "terminal"
|
||||
assert terminal["args"] == ["--setup"]
|
||||
|
||||
def test_build_auth_methods_returns_terminal_setup_when_unconfigured(self, monkeypatch):
|
||||
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: None)
|
||||
|
||||
methods = build_auth_methods()
|
||||
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in methods]
|
||||
|
||||
assert payloads == [
|
||||
{
|
||||
"args": ["--setup"],
|
||||
"description": (
|
||||
"Open Hermes' interactive model/provider setup in a terminal. "
|
||||
"Use this when Hermes has not been configured on this machine yet."
|
||||
),
|
||||
"id": TERMINAL_SETUP_AUTH_METHOD_ID,
|
||||
"name": "Configure Hermes provider",
|
||||
"type": "terminal",
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,39 @@ def test_main_enables_unstable_protocol(monkeypatch):
|
|||
monkeypatch.setattr(entry, "_load_env", lambda: None)
|
||||
monkeypatch.setattr(acp, "run_agent", fake_run_agent)
|
||||
|
||||
entry.main()
|
||||
entry.main([])
|
||||
|
||||
assert calls["kwargs"]["use_unstable_protocol"] is True
|
||||
|
||||
|
||||
def test_main_version_prints_without_starting_server(monkeypatch, capsys):
|
||||
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
|
||||
|
||||
entry.main(["--version"])
|
||||
|
||||
output = capsys.readouterr().out.strip()
|
||||
assert output
|
||||
assert "Starting hermes-agent ACP adapter" not in output
|
||||
|
||||
|
||||
def test_main_check_prints_ok_without_starting_server(monkeypatch, capsys):
|
||||
monkeypatch.setattr(entry, "_setup_logging", lambda: (_ for _ in ()).throw(AssertionError("started server")))
|
||||
|
||||
entry.main(["--check"])
|
||||
|
||||
assert capsys.readouterr().out.strip() == "Hermes ACP check OK"
|
||||
|
||||
|
||||
def test_main_setup_runs_model_configuration(monkeypatch):
|
||||
calls = {}
|
||||
|
||||
def fake_hermes_main():
|
||||
import sys
|
||||
|
||||
calls["argv"] = sys.argv[:]
|
||||
|
||||
monkeypatch.setattr("hermes_cli.main.main", fake_hermes_main)
|
||||
|
||||
entry.main(["--setup"])
|
||||
|
||||
assert calls["argv"][1:] == ["model"]
|
||||
|
|
|
|||
96
tests/acp/test_registry_manifest.py
Normal file
96
tests/acp/test_registry_manifest.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Tests for ACP Registry metadata shipped with Hermes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MANIFEST = ROOT / "acp_registry" / "agent.json"
|
||||
ICON = ROOT / "acp_registry" / "icon.svg"
|
||||
FORBIDDEN_MANIFEST_KEYS = {"schema_version", "display_name"}
|
||||
ALLOWED_DISTRIBUTIONS = {"binary", "npx", "uvx"}
|
||||
|
||||
|
||||
def _manifest() -> dict:
|
||||
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _pyproject_version() -> str:
|
||||
data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
return data["project"]["version"]
|
||||
|
||||
|
||||
def test_agent_json_matches_official_registry_required_fields():
|
||||
data = _manifest()
|
||||
|
||||
assert FORBIDDEN_MANIFEST_KEYS.isdisjoint(data)
|
||||
assert data["id"] == "hermes-agent"
|
||||
assert re.fullmatch(r"[a-z][a-z0-9-]*", data["id"])
|
||||
assert data["name"] == "Hermes Agent"
|
||||
assert data["description"]
|
||||
assert data["repository"] == "https://github.com/NousResearch/hermes-agent"
|
||||
assert data["website"].startswith("https://hermes-agent.nousresearch.com/")
|
||||
assert data["authors"] == ["Nous Research"]
|
||||
assert data["license"] == "MIT"
|
||||
assert set(data["distribution"]) <= ALLOWED_DISTRIBUTIONS
|
||||
|
||||
|
||||
def test_agent_json_uses_npx_distribution_without_local_command_fields():
|
||||
data = _manifest()
|
||||
|
||||
assert set(data["distribution"]) == {"npx"}
|
||||
assert set(data["distribution"]["npx"]) == {"package"}
|
||||
assert data["distribution"]["npx"]["package"] == (
|
||||
f"@nousresearch/hermes-agent-acp@{data['version']}"
|
||||
)
|
||||
assert "type" not in data["distribution"]
|
||||
assert "command" not in data["distribution"]
|
||||
assert "args" not in data["distribution"]
|
||||
|
||||
|
||||
def test_agent_json_version_matches_pyproject():
|
||||
assert _manifest()["version"] == _pyproject_version()
|
||||
|
||||
|
||||
def test_npm_launcher_versions_match_pyproject_and_manifest():
|
||||
version = _pyproject_version()
|
||||
package = json.loads(
|
||||
(ROOT / "packages" / "hermes-agent-acp" / "package.json").read_text(encoding="utf-8")
|
||||
)
|
||||
launcher = (ROOT / "packages" / "hermes-agent-acp" / "bin" / "hermes-agent-acp.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert package["version"] == version
|
||||
assert f"const HERMES_AGENT_VERSION = '{version}';" in launcher
|
||||
assert _manifest()["distribution"]["npx"]["package"] == (
|
||||
f"@nousresearch/hermes-agent-acp@{version}"
|
||||
)
|
||||
|
||||
|
||||
def test_icon_svg_is_16x16_current_color():
|
||||
root = ET.fromstring(ICON.read_text(encoding="utf-8"))
|
||||
|
||||
assert root.attrib["viewBox"] == "0 0 16 16"
|
||||
assert root.attrib["width"] == "16"
|
||||
assert root.attrib["height"] == "16"
|
||||
|
||||
|
||||
def test_icon_svg_has_no_hardcoded_colors_or_gradients():
|
||||
text = ICON.read_text(encoding="utf-8")
|
||||
|
||||
assert "linearGradient" not in text
|
||||
assert "radialGradient" not in text
|
||||
assert "url(#" not in text
|
||||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", text)
|
||||
|
||||
root = ET.fromstring(text)
|
||||
for element in root.iter():
|
||||
for attr in ("fill", "stroke"):
|
||||
value = element.attrib.get(attr)
|
||||
if value is not None:
|
||||
assert value in {"currentColor", "none"}
|
||||
|
|
@ -33,6 +33,7 @@ from acp.schema import (
|
|||
UsageUpdate,
|
||||
UserMessageChunk,
|
||||
)
|
||||
from acp_adapter.auth import TERMINAL_SETUP_AUTH_METHOD_ID
|
||||
from acp_adapter.server import HermesACPAgent, HERMES_VERSION
|
||||
from acp_adapter.session import SessionManager
|
||||
from hermes_state import SessionDB
|
||||
|
|
@ -92,6 +93,41 @@ class TestInitialize:
|
|||
assert "list" in session_caps
|
||||
assert "resume" in session_caps
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_advertises_provider_and_terminal_auth_methods(self, agent, monkeypatch):
|
||||
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: "openrouter")
|
||||
monkeypatch.setattr("acp_adapter.server.detect_provider", lambda: "openrouter")
|
||||
|
||||
resp = await agent.initialize(protocol_version=1)
|
||||
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in resp.auth_methods]
|
||||
|
||||
assert payloads[0]["id"] == "openrouter"
|
||||
assert payloads[0]["name"] == "openrouter runtime credentials"
|
||||
terminal = next(payload for payload in payloads if payload["id"] == TERMINAL_SETUP_AUTH_METHOD_ID)
|
||||
assert terminal["type"] == "terminal"
|
||||
assert terminal["args"] == ["--setup"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_advertises_terminal_setup_auth_when_no_provider(self, agent, monkeypatch):
|
||||
monkeypatch.setattr("acp_adapter.auth.detect_provider", lambda: None)
|
||||
monkeypatch.setattr("acp_adapter.server.detect_provider", lambda: None)
|
||||
|
||||
resp = await agent.initialize(protocol_version=1)
|
||||
payloads = [method.model_dump(by_alias=True, exclude_none=True) for method in resp.auth_methods]
|
||||
|
||||
assert payloads == [
|
||||
{
|
||||
"args": ["--setup"],
|
||||
"description": (
|
||||
"Open Hermes' interactive model/provider setup in a terminal. "
|
||||
"Use this when Hermes has not been configured on this machine yet."
|
||||
),
|
||||
"id": TERMINAL_SETUP_AUTH_METHOD_ID,
|
||||
"name": "Configure Hermes provider",
|
||||
"type": "terminal",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# authenticate
|
||||
|
|
@ -135,6 +171,24 @@ class TestAuthenticate:
|
|||
resp = await agent.authenticate(method_id="openrouter")
|
||||
assert resp is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_accepts_terminal_setup_after_provider_configured(self, agent, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"acp_adapter.server.detect_provider",
|
||||
lambda: "openrouter",
|
||||
)
|
||||
resp = await agent.authenticate(method_id=TERMINAL_SETUP_AUTH_METHOD_ID)
|
||||
assert isinstance(resp, AuthenticateResponse)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_rejects_terminal_setup_without_provider(self, agent, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"acp_adapter.server.detect_provider",
|
||||
lambda: None,
|
||||
)
|
||||
resp = await agent.authenticate(method_id=TERMINAL_SETUP_AUTH_METHOD_ID)
|
||||
assert resp is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# new_session / cancel / load / resume
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue