feat: generate the tool catalog from the pinned swagger
Adds scripts/generate_catalog.py (with --check), the naming rules, schema conversion, the committed tools.json for all 537 operations and the load_catalog() loader.
This commit is contained in:
1 parent
a807a70556
commit
041b3ea7ff
9 files changed
+2245
-2
No files matched your search
@@ -0,0 +1,151 @@
|
||||
"""Tests for the generator script (--check drift detection) and the loader."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from aegis_gitea_mcp.catalog import DOMAINS, Catalog, Operation, load_catalog
|
||||
from aegis_gitea_mcp.catalog.build import (
|
||||
diff_summary,
|
||||
generate,
|
||||
render_catalog,
|
||||
spec_sha256,
|
||||
)
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC_PATH = REPO_ROOT / "spec" / "gitea-swagger.v1.json"
|
||||
CATALOG_PATH = REPO_ROOT / "src" / "aegis_gitea_mcp" / "catalog" / "tools.json"
|
||||
SCRIPT_PATH = REPO_ROOT / "scripts" / "generate_catalog.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def script() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("generate_catalog_script", SCRIPT_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def generated() -> str:
|
||||
text, _ = generate(SPEC_PATH)
|
||||
return text
|
||||
|
||||
|
||||
def test_committed_catalog_is_up_to_date(generated: str) -> None:
|
||||
assert CATALOG_PATH.read_text("utf-8") == generated
|
||||
|
||||
|
||||
def test_catalog_header_matches_pinned_spec() -> None:
|
||||
document = json.loads(CATALOG_PATH.read_text("utf-8"))
|
||||
assert document["spec"] == {
|
||||
"title": "Gitea API",
|
||||
"version": "28.1.0",
|
||||
"sha256": spec_sha256(SPEC_PATH.read_bytes()),
|
||||
}
|
||||
assert len(document["operations"]) == 537
|
||||
|
||||
|
||||
def test_check_passes_on_fresh_file(
|
||||
script: ModuleType, generated: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
target = tmp_path / "tools.json"
|
||||
target.write_text(generated, "utf-8")
|
||||
assert script.main(["--check"], SPEC_PATH, target) == 0
|
||||
assert "up to date" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_check_detects_drift(
|
||||
script: ModuleType, generated: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
target = tmp_path / "tools.json"
|
||||
target.write_text(
|
||||
generated.replace("gitea_repo_list_branches", "gitea_repo_list_branch"), "utf-8"
|
||||
)
|
||||
assert script.main(["--check"], SPEC_PATH, target) == 1
|
||||
err = capsys.readouterr().err
|
||||
assert "out of date" in err
|
||||
assert "changed lines" in err
|
||||
|
||||
|
||||
def test_check_fails_when_file_is_missing(script: ModuleType, tmp_path: Path) -> None:
|
||||
assert script.main(["--check"], SPEC_PATH, tmp_path / "missing.json") == 1
|
||||
|
||||
|
||||
def test_check_detects_spec_change(script: ModuleType, generated: str, tmp_path: Path) -> None:
|
||||
target = tmp_path / "tools.json"
|
||||
target.write_text(generated, "utf-8")
|
||||
changed_spec = tmp_path / "spec.json"
|
||||
spec = json.loads(SPEC_PATH.read_text("utf-8"))
|
||||
spec["info"]["version"] = "99.0.0"
|
||||
changed_spec.write_text(json.dumps(spec), "utf-8")
|
||||
assert script.main(["--check"], changed_spec, target) == 1
|
||||
|
||||
|
||||
def test_write_mode_creates_identical_file(
|
||||
script: ModuleType, generated: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
target = tmp_path / "out" / "tools.json"
|
||||
target.parent.mkdir()
|
||||
assert script.main([], SPEC_PATH, target) == 0
|
||||
assert target.read_text("utf-8") == generated
|
||||
out = capsys.readouterr().out
|
||||
assert "operations per domain" in out
|
||||
assert "total 537" in out
|
||||
assert "override:" in out
|
||||
shutil.rmtree(target.parent)
|
||||
|
||||
|
||||
def test_diff_summary_truncates() -> None:
|
||||
old = "\n".join(f"line {i}" for i in range(100))
|
||||
new = "\n".join(f"changed {i}" for i in range(100))
|
||||
summary = diff_summary(old, new, limit=5)
|
||||
assert summary.splitlines()[0] == "200 changed lines"
|
||||
assert "more changed lines" in summary
|
||||
|
||||
|
||||
def test_loader_round_trip() -> None:
|
||||
catalog = load_catalog()
|
||||
assert isinstance(catalog, Catalog)
|
||||
assert catalog.spec_version == "28.1.0"
|
||||
assert len(catalog.operations) == 537
|
||||
document = json.loads(CATALOG_PATH.read_text("utf-8"))
|
||||
assert [operation.to_dict() for operation in catalog.operations] == document["operations"]
|
||||
assert render_catalog(document) == CATALOG_PATH.read_text("utf-8")
|
||||
for operation in catalog.operations:
|
||||
assert Operation.from_dict(operation.to_dict()) == operation
|
||||
|
||||
|
||||
def test_loader_is_cached_and_indexed() -> None:
|
||||
assert load_catalog() is load_catalog()
|
||||
catalog = load_catalog()
|
||||
operation = catalog.get("gitea_repo_list_branches")
|
||||
assert operation is not None
|
||||
assert operation.operation_id == "repoListBranches"
|
||||
assert operation.path == "/repos/{owner}/{repo}/branches"
|
||||
assert operation.method == "GET"
|
||||
assert catalog.get("gitea_nope_nothing") is None
|
||||
names = [item.tool_name for item in catalog.operations]
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
def test_loader_domains() -> None:
|
||||
assert load_catalog().domains() == sorted(DOMAINS, key=DOMAINS.index)
|
||||
assert set(load_catalog().domains()) == set(DOMAINS)
|
||||
empty = Catalog(spec_version="0", operations=())
|
||||
assert empty.domains() == []
|
||||
assert empty.get("gitea_repo_get") is None
|
||||
|
||||
|
||||
def test_tools_json_ships_in_the_package() -> None:
|
||||
from importlib import resources
|
||||
|
||||
assert resources.files("aegis_gitea_mcp.catalog").joinpath("tools.json").is_file()
|
||||
Reference in new issue
Block a user