Download tests/website/test_extract_plugins.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/website/test_extract_plugins.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/tests/website/test_extract_plugins.py
-
curl -L -o test_extract_plugins.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/website/test_extract_plugins.py
8.01 kB
| """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" | |
| 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" | |
| # -------------------------------------------------------------------------- | |
| # 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: gamma outranks beta within the community tier. | |
| (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] == ["alpha", "gamma", "beta"] # official first, then stars desc | |
| 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() | |