mirror of
https://github.com/wshobson/agents.git
synced 2026-09-14 18:04:20 +08:00
refactor(garden): one entry rule for both marketplace readers
Review feedback on PR #676. actual_counts counted {"name": ["bad"]} because it is a dictionary, while check_marketplace_consistency rejected the same entry as MARKETPLACE_SHAPE. A counts-only run could report a stale total from a manifest the other check calls invalid. Rather than copy the name rule into the second place, both now call marketplace_entry_problem, which returns why an entry is unusable or None. That also collapses the two near-identical report blocks in the consistency check into one. Parametrized coverage of the rule itself, plus a case proving a list-valued name leaves the count unknown.
This commit is contained in:
+24
-14
@@ -105,6 +105,21 @@ class Report:
|
||||
return [f for f in self.findings if f.severity == severity]
|
||||
|
||||
|
||||
def marketplace_entry_problem(entry: object) -> str | None:
|
||||
"""Say why a `plugins[]` entry is unusable, or None when it is fine.
|
||||
|
||||
Both readers of the manifest go through this. When only one of them enforced the
|
||||
shape, a counts-only run could report a stale total from a manifest the
|
||||
consistency check rejects.
|
||||
"""
|
||||
if not isinstance(entry, dict):
|
||||
return f" is {type(entry).__name__}, expected an object"
|
||||
name = entry.get("name")
|
||||
if name is not None and not isinstance(name, str):
|
||||
return f".name is {type(name).__name__}, expected a string"
|
||||
return None
|
||||
|
||||
|
||||
def read_text_or_none(path: Path, report: Report) -> str | None:
|
||||
"""Read a file as UTF-8, reporting a finding rather than killing the whole run.
|
||||
|
||||
@@ -405,26 +420,18 @@ def check_marketplace_consistency(report: Report) -> None:
|
||||
)
|
||||
return
|
||||
for position, raw_entry in enumerate(entries):
|
||||
if not isinstance(raw_entry, dict):
|
||||
problem = marketplace_entry_problem(raw_entry)
|
||||
if problem is not None:
|
||||
report.add(
|
||||
kind="MARKETPLACE_SHAPE",
|
||||
severity="error",
|
||||
path=MARKETPLACE_JSON,
|
||||
message=f"plugins[{position}] is {type(raw_entry).__name__}, expected an object",
|
||||
message=f"plugins[{position}]{problem}",
|
||||
fix="Remove the entry or give it the usual name/source/description fields.",
|
||||
)
|
||||
continue
|
||||
entry = cast("dict[str, Any]", raw_entry)
|
||||
name = entry.get("name")
|
||||
if name is not None and not isinstance(name, str):
|
||||
report.add(
|
||||
kind="MARKETPLACE_SHAPE",
|
||||
severity="error",
|
||||
path=MARKETPLACE_JSON,
|
||||
message=f"plugins[{position}].name is {type(name).__name__}, expected a string",
|
||||
fix="Give the entry a plain string name.",
|
||||
)
|
||||
continue
|
||||
if not name:
|
||||
continue
|
||||
source = entry.get("source")
|
||||
@@ -508,9 +515,12 @@ def actual_counts(report: Report) -> dict[str, int | None]:
|
||||
# Valid JSON of the wrong shape is still an unknown count, not a crash.
|
||||
if isinstance(manifest, dict):
|
||||
entries = manifest.get("plugins")
|
||||
# A list holding a non-object entry is malformed. Counting it would let
|
||||
# garbage drive an error-severity finding.
|
||||
if isinstance(entries, list) and all(isinstance(e, dict) for e in entries):
|
||||
# Malformed entries make the total unknown. Counting them would let
|
||||
# garbage drive an error-severity finding, and would disagree with
|
||||
# check_marketplace_consistency about the same manifest.
|
||||
if isinstance(entries, list) and all(
|
||||
marketplace_entry_problem(e) is None for e in entries
|
||||
):
|
||||
plugins = len(entries)
|
||||
return {
|
||||
"plugins": plugins,
|
||||
|
||||
@@ -10,6 +10,7 @@ from tools.doc_gardener import (
|
||||
CHECKS,
|
||||
Report,
|
||||
actual_counts,
|
||||
marketplace_entry_problem,
|
||||
check_agent_divergence,
|
||||
check_codex_skill_caps,
|
||||
check_dead_links,
|
||||
@@ -506,6 +507,22 @@ class TestDocCounts:
|
||||
assert [f for f in report.findings if f.kind == "STALE_COUNT"] == []
|
||||
assert actual_counts(Report())["plugins"] is None
|
||||
|
||||
def test_list_valued_name_makes_the_count_unknown(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Both readers of the manifest apply the same entry rule."""
|
||||
_patch_paths(monkeypatch, tmp_path)
|
||||
_write_counts_fixture(tmp_path, plugins=12, agents=34)
|
||||
(tmp_path / ".claude-plugin" / "marketplace.json").write_text(
|
||||
'{"plugins": [{"name": ["bad"]}]}'
|
||||
)
|
||||
(tmp_path / "README.md").write_text("We ship 99 plugins today.\n")
|
||||
|
||||
report = Report()
|
||||
check_doc_counts(report)
|
||||
assert [f for f in report.findings if f.kind == "STALE_COUNT"] == []
|
||||
assert actual_counts(Report())["plugins"] is None
|
||||
|
||||
def test_docs_subtotals_are_not_scanned(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Per-category subtotals under docs/ legitimately differ from the totals."""
|
||||
_patch_paths(monkeypatch, tmp_path)
|
||||
@@ -848,3 +865,24 @@ class TestMarketplaceShape:
|
||||
report = Report()
|
||||
check_marketplace_consistency(report) # must not raise
|
||||
assert [f for f in report.findings if f.kind == "MARKETPLACE_SHAPE"]
|
||||
|
||||
|
||||
# ── Shared entry rule ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entry", "ok"),
|
||||
[
|
||||
({"name": "x", "source": "./plugins/x"}, True),
|
||||
({"source": "./plugins/x"}, True), # name is optional
|
||||
({"name": ""}, True), # empty name is skipped downstream, not malformed
|
||||
({"name": ["bad"]}, False),
|
||||
({"name": 7}, False),
|
||||
(None, False),
|
||||
([], False),
|
||||
("string", False),
|
||||
],
|
||||
)
|
||||
def test_marketplace_entry_problem(entry: object, ok: bool):
|
||||
"""One rule, so the counts check and the consistency check cannot disagree."""
|
||||
assert (marketplace_entry_problem(entry) is None) is ok
|
||||
|
||||
Reference in New Issue
Block a user