From f22a47003f8dae00f30b8782f722762388707a8c Mon Sep 17 00:00:00 2001 From: Parintorn Sukhowatanakit Date: Thu, 1 Oct 2026 05:03:28 +0700 Subject: [PATCH] Tests check the manifests' wiring, and CI runs weekly claude plugin validate --strict passes a monitors.json that is not JSON and a hook that runs a subcommand mailbox.py does not have. test_manifests.py parses every JSON manifest and checks each hook and monitor command against the subcommands the script dispatches on. CI also runs every Monday, since the validate job takes the latest Claude Code. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 3 +++ CONTRIBUTING.md | 2 ++ tests/test_manifests.py | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/test_manifests.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58b99e2..f40086c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: branches: [main] pull_request: + # The validate job installs the latest Claude Code, whose checks can tighten with no commit here. + schedule: + - cron: "0 3 * * 1" permissions: contents: read diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eef2ce..89a9444 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,8 @@ installed one. - A hook must never fail the session: errors go to stderr and the exit code stays 0. - Add or update tests in `tests/test_mailbox.py` with any change to the script. Tests set `ANIMUS_AGENTS_DIR` to a temporary folder, so they never touch the real `~/.agent-mailbox`. +- `tests/test_manifests.py` checks what `claude plugin validate` does not: every JSON manifest + parses, and each hook and monitor command runs a subcommand `scripts/mailbox.py` has. - Bump `version` in `.claude-plugin/plugin.json` when a change should reach installed copies. ## Commits diff --git a/tests/test_manifests.py b/tests/test_manifests.py new file mode 100644 index 0000000..b279223 --- /dev/null +++ b/tests/test_manifests.py @@ -0,0 +1,47 @@ +"""The wiring `claude plugin validate` does not check: every manifest parses, and each hook and monitor +command runs a subcommand scripts/mailbox.py dispatches on.""" +import json +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = ROOT / "scripts" / "mailbox.py" +CALL = re.compile(r'"\$\{CLAUDE_PLUGIN_ROOT\}/(\S+?)" (\S+)') + + +def commands(node): + if isinstance(node, dict): + for key, value in node.items(): + if key == "command": + yield value + else: + yield from commands(value) + elif isinstance(node, list): + for value in node: + yield from commands(value) + + +class Manifests(unittest.TestCase): + def test_every_manifest_parses(self): + found = [p for p in ROOT.rglob("*.json") if ".git" not in p.parts] + self.assertGreaterEqual(len(found), 4) + for path in found: + with self.subTest(path=path.relative_to(ROOT)): + json.loads(path.read_text(encoding="utf-8")) + + def test_hooks_and_monitors_run_subcommands_the_script_has(self): + source = SCRIPT.read_text(encoding="utf-8") + dispatched = set(re.findall(r'command == "([\w-]+)"', source)) + self.assertIn("session-start", dispatched) + for manifest in ("hooks/hooks.json", "monitors/monitors.json"): + for command in commands(json.loads((ROOT / manifest).read_text(encoding="utf-8"))): + with self.subTest(manifest=manifest, command=command): + match = CALL.search(command) + self.assertIsNotNone(match, "runs no script from the plugin") + self.assertTrue((ROOT / match[1]).is_file(), match[1]) + self.assertIn(match[2], dispatched) + + +if __name__ == "__main__": + unittest.main()