Files
hermes-agent/tests/website/test_extract_plugins.py
teknium1 76fe8f7f5f fix: plugin catalog ranks by stars only and drops the review-claim badges
The catalog hero showed three trust chips ("Reviewed by the Hermes team",
"Installs exactly the version we reviewed", "One click from Hermes Desktop")
and the hero copy repeated the review claim. Teknium asked for the review
claim to go; the row goes with it since the remaining two chips only made
sense as a set.

Ranking pinned the official tier above every community entry, so a 5-star
official plugin sat above a 215-star community one and the page looked
unsorted. Tier no longer ranks; stars desc, unknown stars last, name as the
tiebreak. The Official/Community filter pills still exist for anyone who
wants the tier view.
2026-09-19 22:45:39 -07:00

222 lines
8.6 KiB
Python

"""Tests for website/scripts/extract-plugins.py.
Behavioral contracts for the /docs/plugins catalog extractor:
1. Reads ``plugin-catalog/*.yaml`` entries (skipping ``removed.yaml``) and
emits ``plugins.json`` rows carrying name/repo/sha/tier/capabilities plus
a synthesized ``hermes plugins install <name>`` command.
2. Entries missing any of name/repo/sha are skipped (logged, not fatal).
3. A missing ``plugin-catalog/`` directory degrades gracefully: empty
catalog list, zero counts in the meta sidecar, exit 0 — the docs build
must stay green before the catalog directory lands on main.
"""
from __future__ import annotations
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
EXTRACT = REPO_ROOT / "website" / "scripts" / "extract-plugins.py"
@pytest.fixture(scope="module")
def mod():
spec = importlib.util.spec_from_file_location("extract_plugins", EXTRACT)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _write_entry(catalog_dir: Path, name: str, **overrides) -> Path:
import yaml
entry = {
"name": name,
"repo": f"https://github.com/example/{name}",
"sha": "38fe0fb53eff98d477f807432e965429e665ca33",
"description": f"{name} does things.",
"maintainer": "Example",
"tier": "community",
}
entry.update(overrides)
# Drop keys explicitly set to None so tests can simulate missing fields.
entry = {k: v for k, v in entry.items() if v is not None}
path = catalog_dir / f"{name}.yaml"
path.write_text(yaml.safe_dump(entry), encoding="utf-8")
return path
# --------------------------------------------------------------------------
# Entry loading + validation
# --------------------------------------------------------------------------
def test_valid_entry_is_extracted_with_install_command(mod, tmp_path):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(
catalog,
"example-plugin",
tier="official",
docs_url="https://example.com/docs",
requires_hermes=">=0.19",
platforms=["linux"],
capabilities={
"provides_tools": ["do_thing"],
"provides_hooks": ["on_start"],
"provides_middleware": [],
"requires_env": ["EXAMPLE_TOKEN"],
},
)
entries = mod.load_catalog_entries(catalog)
assert len(entries) == 1
e = entries[0]
assert e["name"] == "example-plugin"
assert e["repo"] == "https://github.com/example/example-plugin"
assert e["sha"] == "38fe0fb53eff98d477f807432e965429e665ca33"
assert e["shaShort"] == "38fe0fb"
assert e["tier"] == "official"
assert e["maintainer"] == "Example"
assert e["requiresHermes"] == ">=0.19"
assert e["platforms"] == ["linux"]
assert e["docsUrl"] == "https://example.com/docs"
assert e["capabilities"]["providesTools"] == ["do_thing"]
assert e["capabilities"]["providesHooks"] == ["on_start"]
assert e["capabilities"]["requiresEnv"] == ["EXAMPLE_TOKEN"]
assert e["installCommand"] == "hermes plugins install example-plugin"
def test_entries_missing_required_fields_are_skipped(mod, tmp_path, capsys):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(catalog, "good-plugin")
_write_entry(catalog, "no-sha", sha=None)
_write_entry(catalog, "no-repo", repo=None)
entries = mod.load_catalog_entries(catalog)
assert [e["name"] for e in entries] == ["good-plugin"]
err = capsys.readouterr().err
assert "no-sha" in err
assert "no-repo" in err
def test_removed_yaml_is_not_treated_as_an_entry(mod, tmp_path):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(catalog, "kept-plugin")
(catalog / "removed.yaml").write_text(
"removed:\n - name: evil-plugin\n repo: https://github.com/evil/x\n"
' reason: "bad"\n date: "2026-07-02"\n',
encoding="utf-8",
)
entries = mod.load_catalog_entries(catalog)
assert [e["name"] for e in entries] == ["kept-plugin"]
assert mod.count_removed(catalog) == 1
def test_unknown_tier_normalizes_to_community(mod, tmp_path):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(catalog, "weird-tier", tier="platinum")
entries = mod.load_catalog_entries(catalog)
assert entries[0]["tier"] == "community"
def test_version_and_image_are_emitted_and_offhost_image_is_dropped_not_fatal(mod, tmp_path):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(catalog, "labelled", version="1.4.0", image="https://raw.githubusercontent.com/owner/repo/38fe0fb53eff98d477f807432e965429e665ca33/banner.png")
_write_entry(catalog, "offhost", version="1.4.0", image="https://cdn.example.com/banner.png")
entries = {e["name"]: e for e in mod.load_catalog_entries(catalog)}
assert entries["labelled"]["version"] == "1.4.0" and entries["labelled"]["image"] == "https://raw.githubusercontent.com/owner/repo/38fe0fb53eff98d477f807432e965429e665ca33/banner.png"
assert entries["offhost"]["image"] == "" and entries["offhost"]["version"] == "1.4.0"
# --------------------------------------------------------------------------
# Full run: outputs + graceful degradation
# --------------------------------------------------------------------------
def test_main_writes_catalog_and_meta(mod, tmp_path):
catalog = tmp_path / "plugin-catalog"
catalog.mkdir()
_write_entry(catalog, "alpha", tier="official", category="memory")
_write_entry(catalog, "beta") # no category → default "desktop" shelf
_write_entry(catalog, "gamma")
# Star cache from fetch-plugin-stars.py: ranking is stars only; the official tier gets no boost.
(tmp_path / "api").mkdir()
(tmp_path / "api" / "plugin-stars.json").write_text(json.dumps({
"fetched_at": "2026-09-15T00:00:00+00:00",
"stars": {"example/gamma": 50, "example/beta": 3}}), encoding="utf-8")
(catalog / "removed.yaml").write_text(
"removed:\n - name: gone\n", encoding="utf-8"
)
out_dir = tmp_path / "api"
rc = mod.main(catalog_dir=catalog, output_dir=out_dir)
assert rc == 0
plugins = json.loads((out_dir / "plugins.json").read_text(encoding="utf-8"))
meta = json.loads((out_dir / "plugins-meta.json").read_text(encoding="utf-8"))
assert [p["name"] for p in plugins] == ["gamma", "beta", "alpha"] # stars desc, unknown stars last
assert {p["name"]: p["stars"] for p in plugins} == {"alpha": None, "gamma": 50, "beta": 3}
assert meta["total"] == 3
assert meta["byTier"] == {"official": 1, "community": 2}
assert {p["name"]: p["category"] for p in plugins} == {"alpha": "memory", "beta": "desktop", "gamma": "desktop"}
assert meta["byCategory"] == {"desktop": 2, "memory": 1}
assert meta["starsFetchedAt"] == "2026-09-15T00:00:00+00:00"
assert meta["removedCount"] == 1
assert meta["generatedAt"]
# The live-refresh document consumed by installed clients: loader-schema entries + the kill list.
from hermes_cli.plugin_catalog import entry_from_mapping
live = json.loads((out_dir / "plugin-catalog.json").read_text(encoding="utf-8"))
assert [entry_from_mapping(raw, "live").name for raw in live["entries"]] == ["alpha", "beta", "gamma"]
assert live["removed"] == [{"name": "gone"}]
def test_missing_catalog_dir_degrades_to_empty_outputs_exit_zero(mod, tmp_path):
out_dir = tmp_path / "api"
rc = mod.main(catalog_dir=tmp_path / "does-not-exist", output_dir=out_dir)
assert rc == 0
plugins = json.loads((out_dir / "plugins.json").read_text(encoding="utf-8"))
meta = json.loads((out_dir / "plugins-meta.json").read_text(encoding="utf-8"))
assert plugins == []
assert meta["total"] == 0
assert meta["byTier"] == {"official": 0, "community": 0}
assert meta["removedCount"] == 0
def test_script_exits_zero_as_subprocess_when_catalog_missing(tmp_path):
"""CLI contract: the deploy step runs the script hard (no `|| true`);
it must exit 0 even when plugin-catalog/ hasn't landed yet."""
out_dir = tmp_path / "api"
result = subprocess.run(
[
sys.executable,
str(EXTRACT),
"--catalog-dir",
str(tmp_path / "missing"),
"--output-dir",
str(out_dir),
],
capture_output=True,
text=True,
timeout=60,
)
assert result.returncode == 0, result.stderr
assert (out_dir / "plugins.json").exists()
assert (out_dir / "plugins-meta.json").exists()