forked from Zakaria/hermes-agent
Hermes-agent
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Invariants for scripts/build_skills_index.py's health-check guard.
|
||||
|
||||
Regression context (June 2026): a GitHub API rate limit zeroed every
|
||||
api.github.com-backed source (github / claude-marketplace / well-known) at
|
||||
once during the docs deploy crawl. The build's health check fired and exited
|
||||
non-zero — but it had ALREADY written the degenerate index to disk, and
|
||||
deploy-site.yml swallowed the exit code with ``|| echo non-fatal``. The
|
||||
partial index (missing the OpenAI/Anthropic/HuggingFace/NVIDIA tabs) shipped
|
||||
to the live Skills Hub.
|
||||
|
||||
These tests pin the two contracts that prevent a recurrence:
|
||||
1. A degenerate crawl exits non-zero AND does NOT write the output file
|
||||
(so extract-skills.py falls back instead of reading a broken index).
|
||||
2. A healthy crawl exits zero AND writes the file with every source present.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
import scripts.build_skills_index as build_mod
|
||||
|
||||
|
||||
def _meta(name, src):
|
||||
return build_mod.SkillMeta(
|
||||
name=name, description="d", source=src,
|
||||
identifier=f"{src}/{name}", trust_level="community",
|
||||
)
|
||||
|
||||
|
||||
class _FakeSource:
|
||||
def __init__(self, src, n, rate_limited=False):
|
||||
self._src = src
|
||||
self._n = n
|
||||
self.is_rate_limited = rate_limited
|
||||
|
||||
def search(self, query, limit=10):
|
||||
return [_meta(f"{self._src}-{i}", self._src) for i in range(self._n)]
|
||||
|
||||
|
||||
def _install_fake_sources(monkeypatch, *, github_count, claude_count=40,
|
||||
well_known_count=10, github_rate_limited=False):
|
||||
monkeypatch.setattr(build_mod, "SkillsShSource", lambda auth: _FakeSource("skills.sh", 15000))
|
||||
monkeypatch.setattr(build_mod, "OptionalSkillSource", lambda: _FakeSource("official", 95))
|
||||
monkeypatch.setattr(build_mod, "WellKnownSkillSource", lambda: _FakeSource("well-known", well_known_count))
|
||||
monkeypatch.setattr(
|
||||
build_mod, "GitHubSource",
|
||||
lambda auth: _FakeSource("github", github_count, rate_limited=github_rate_limited),
|
||||
)
|
||||
monkeypatch.setattr(build_mod, "ClawHubSource", lambda: _FakeSource("clawhub", 69000))
|
||||
monkeypatch.setattr(
|
||||
build_mod, "ClaudeMarketplaceSource",
|
||||
lambda auth: _FakeSource("claude-marketplace", claude_count, rate_limited=github_rate_limited),
|
||||
)
|
||||
monkeypatch.setattr(build_mod, "LobeHubSource", lambda: _FakeSource("lobehub", 500))
|
||||
monkeypatch.setattr(build_mod, "BrowseShSource", lambda: _FakeSource("browse-sh", 380))
|
||||
monkeypatch.setattr(
|
||||
build_mod, "crawl_skills_sh",
|
||||
lambda source: [build_mod._meta_to_dict(m) for m in source.search("", 0)],
|
||||
)
|
||||
monkeypatch.setattr(build_mod, "batch_resolve_paths", lambda skills, auth: skills)
|
||||
monkeypatch.setattr(
|
||||
build_mod, "GitHubAuth",
|
||||
lambda: types.SimpleNamespace(auth_method=lambda: "token"),
|
||||
)
|
||||
|
||||
|
||||
def test_degenerate_crawl_exits_nonzero_and_writes_no_file(tmp_path, monkeypatch):
|
||||
"""A collapsed GitHub crawl must fail loud and leave OUTPUT_PATH unwritten."""
|
||||
out = tmp_path / "skills-index.json"
|
||||
monkeypatch.setattr(build_mod, "OUTPUT_PATH", str(out))
|
||||
_install_fake_sources(monkeypatch, github_count=0, claude_count=0,
|
||||
well_known_count=0, github_rate_limited=True)
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
build_mod.main()
|
||||
|
||||
assert exc.value.code != 0
|
||||
# The degenerate index must NOT have been written — extract-skills.py
|
||||
# relies on the file's absence to fall back instead of reading garbage.
|
||||
assert not out.exists()
|
||||
|
||||
|
||||
def test_healthy_crawl_writes_index_with_all_sources(tmp_path, monkeypatch):
|
||||
out = tmp_path / "skills-index.json"
|
||||
monkeypatch.setattr(build_mod, "OUTPUT_PATH", str(out))
|
||||
_install_fake_sources(monkeypatch, github_count=200)
|
||||
|
||||
build_mod.main() # exit 0 (no SystemExit)
|
||||
|
||||
assert out.exists()
|
||||
import json
|
||||
data = json.loads(out.read_text())
|
||||
sources = {s["source"] for s in data["skills"]}
|
||||
# Every GitHub-API-backed source that vanished in the regression is present.
|
||||
assert {"github", "claude-marketplace", "well-known"} <= sources
|
||||
assert data["skill_count"] == len(data["skills"])
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Tests for the ACP Registry version-lockstep bump in scripts/release.py.
|
||||
|
||||
The official ACP Registry manifest must match ``pyproject.toml`` exactly —
|
||||
``tests/acp/test_registry_manifest.py`` enforces this at lint time, and the
|
||||
upstream registry CI rejects ``@latest`` / floating pins. The release script
|
||||
is the single place that bumps the manifest in lockstep with pyproject; if
|
||||
that bump ever silently breaks, weekly releases fail the manifest test
|
||||
until someone hand-edits the JSON.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_release_module(monkeypatch, tmp_root: Path):
|
||||
"""Import scripts/release.py with REPO_ROOT pinned to a temp tree."""
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_release_under_test",
|
||||
Path(__file__).resolve().parents[2] / "scripts" / "release.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
monkeypatch.setattr(module, "REPO_ROOT", tmp_root)
|
||||
monkeypatch.setattr(
|
||||
module, "ACP_REGISTRY_MANIFEST", tmp_root / "acp_registry" / "agent.json"
|
||||
)
|
||||
return module
|
||||
|
||||
|
||||
def _write_manifest(root: Path, version: str) -> None:
|
||||
manifest_dir = root / "acp_registry"
|
||||
manifest_dir.mkdir(parents=True)
|
||||
(manifest_dir / "agent.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "hermes-agent",
|
||||
"name": "Hermes Agent",
|
||||
"version": version,
|
||||
"description": "test",
|
||||
"distribution": {
|
||||
"uvx": {
|
||||
"package": f"hermes-agent[acp]=={version}",
|
||||
"args": ["hermes-acp"],
|
||||
}
|
||||
},
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_update_acp_registry_versions_bumps_manifest_and_pin(monkeypatch, tmp_path):
|
||||
_write_manifest(tmp_path, "0.13.0")
|
||||
module = _load_release_module(monkeypatch, tmp_path)
|
||||
|
||||
module._update_acp_registry_versions("0.14.0")
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert manifest["version"] == "0.14.0"
|
||||
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
|
||||
# args stay untouched so we don't accidentally rewrite them.
|
||||
assert manifest["distribution"]["uvx"]["args"] == ["hermes-acp"]
|
||||
|
||||
|
||||
def test_update_acp_registry_versions_is_silent_when_manifest_missing(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""Older release branches predate the ACP Registry asset — must no-op."""
|
||||
module = _load_release_module(monkeypatch, tmp_path)
|
||||
|
||||
# No fixture written; function should not raise.
|
||||
module._update_acp_registry_versions("0.14.0")
|
||||
|
||||
|
||||
def test_update_version_files_bumps_manifest_alongside_pyproject(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""End-to-end: update_version_files() is the function release.py actually
|
||||
calls, so it must drive the manifest bump too."""
|
||||
_write_manifest(tmp_path, "0.13.0")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "hermes-agent"\nversion = "0.13.0"\n', encoding="utf-8"
|
||||
)
|
||||
version_dir = tmp_path / "hermes_cli"
|
||||
version_dir.mkdir()
|
||||
(version_dir / "__init__.py").write_text(
|
||||
'__version__ = "0.13.0"\n__release_date__ = "2026-05-14"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
module = _load_release_module(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(module, "VERSION_FILE", version_dir / "__init__.py")
|
||||
monkeypatch.setattr(module, "PYPROJECT_FILE", tmp_path / "pyproject.toml")
|
||||
|
||||
module.update_version_files("0.14.0", "2026-05-21")
|
||||
|
||||
pyproject_text = (tmp_path / "pyproject.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.14.0"' in pyproject_text
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert manifest["version"] == "0.14.0"
|
||||
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
|
||||
Reference in New Issue
Block a user