Skills/hooks/mcpServers/agents pointer-field validation in plugin.json was fully delegated to sync-plugin-content.sh --check, but that script explicitly skips any plugin directory lacking .apm/ (it has nothing to compile there). A plugin with no .apm/ and a hand-authored plugin.json whose pointer field points at a missing path was therefore left uncovered by either check -- currently latent since every plugin in this repo has .apm/, but a real gap for the first non-apm plugin added. Restores a fallback validation path here for exactly that case (no .apm/ directory), reusing the pre-delegation logic this script used to run unconditionally. apm-native plugins keep relying on the delegated check so the two never duplicate (or disagree) on the same manifest. Also switches the marketplace.json walk to the shared scripts/lib/marketplace-plugins.sh helper introduced alongside sync-plugin-content.sh's matching --all branch, replacing the near-identical hand-duplicated loop this script's own header comment already flagged as a duplication risk. Adds fixtures: a non-apm plugin with a broken skills pointer (caught), a non-apm plugin with a valid pointer (no false positive), and an apm-native plugin with a broken pointer (left to the delegated check, not double-validated here). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
210 lines
7.1 KiB
Bash
210 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SCRIPT="$REPO_ROOT/scripts/check-manifests.sh"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
# Helper: make a minimal valid repo fixture with marketplace + plugin structure.
|
|
# Per ADR-0015/ADR-0017, the manifest check-manifests.sh validates is
|
|
# .claude-plugin/plugin.json (compiled output) -- not the root-level plugin.json,
|
|
# which was deleted repo-wide, and not the skills/hooks/mcpServers/agents pointer
|
|
# fields apm's compiler deliberately never populates (see scripts/check-manifests.sh's
|
|
# own header comment). Content-presence drift is scripts/sync-plugin-content.sh's job.
|
|
make_valid_fixture() {
|
|
local dir
|
|
dir="$(mktemp -d)"
|
|
mkdir -p "$dir/.claude-plugin"
|
|
mkdir -p "$dir/plugins/myplugin/.claude-plugin"
|
|
cat > "$dir/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "myplugin", "source": "./plugins/myplugin" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$dir/plugins/myplugin/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "myplugin"
|
|
}
|
|
JSON
|
|
echo "$dir"
|
|
}
|
|
|
|
# --- 1. Exits 0 against valid repo structure ---
|
|
echo ""
|
|
echo "--- exits 0 when all references are valid ---"
|
|
FIXTURE="$(make_valid_fixture)"
|
|
trap 'rm -rf "$FIXTURE"' EXIT
|
|
if bash "$SCRIPT" "$FIXTURE" > /dev/null 2>&1; then
|
|
pass "exits 0 when all manifest references resolve"
|
|
else
|
|
fail "exited non-zero against a valid fixture"
|
|
fi
|
|
|
|
# --- 2. Exits 1 when plugin source dir is missing ---
|
|
echo ""
|
|
echo "--- exits 1 when plugin source directory missing ---"
|
|
FIXTURE2="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE2"' EXIT
|
|
mkdir -p "$FIXTURE2/.claude-plugin"
|
|
cat > "$FIXTURE2/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "ghost", "source": "./plugins/ghost" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE2" > /dev/null 2>&1; then
|
|
fail "exited 0 when plugin source dir is missing — expected exit 1"
|
|
else
|
|
pass "exits non-zero when plugin source directory does not exist"
|
|
fi
|
|
|
|
# --- 3. Exits 1 when .claude-plugin/plugin.json is missing from plugin dir ---
|
|
echo ""
|
|
echo "--- exits 1 when .claude-plugin/plugin.json missing from plugin directory ---"
|
|
FIXTURE3="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE3"' EXIT
|
|
mkdir -p "$FIXTURE3/.claude-plugin"
|
|
mkdir -p "$FIXTURE3/plugins/nomanifest"
|
|
cat > "$FIXTURE3/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "nomanifest", "source": "./plugins/nomanifest" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE3" > /dev/null 2>&1; then
|
|
fail "exited 0 when .claude-plugin/plugin.json is missing — expected exit 1"
|
|
else
|
|
pass "exits non-zero when .claude-plugin/plugin.json is missing from plugin directory"
|
|
fi
|
|
|
|
# --- 4. Exits 1 when a remote-source plugin entry's local plugin still lacks a manifest ---
|
|
# Remote sources (object-typed `source:`) are skipped entirely; only string (local path)
|
|
# sources are checked. This guards that a mixed marketplace.json still catches a broken
|
|
# local entry alongside a legitimately-skipped remote one.
|
|
echo ""
|
|
echo "--- exits 1 for a broken local entry even when a remote entry is present ---"
|
|
FIXTURE4="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE4"' EXIT
|
|
mkdir -p "$FIXTURE4/.claude-plugin"
|
|
mkdir -p "$FIXTURE4/plugins/broken"
|
|
cat > "$FIXTURE4/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "remote-thing", "source": { "repo": "someorg/somerepo", "source": "github" } },
|
|
{ "name": "broken", "source": "./plugins/broken" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE4" > /dev/null 2>&1; then
|
|
fail "exited 0 with a broken local entry present — expected exit 1"
|
|
else
|
|
pass "exits non-zero for a broken local entry even alongside a skipped remote entry"
|
|
fi
|
|
|
|
# --- 5. Non-.apm/ plugin with a broken pointer field is caught by the fallback path ---
|
|
# apm-native plugins (.apm/ present) get their skills/hooks/mcpServers/agents
|
|
# pointer-field validation from sync-plugin-content.sh --check instead (see this
|
|
# script's header comment) -- but that script skips any plugin dir lacking .apm/
|
|
# outright, so a non-apm plugin's hand-authored plugin.json needs this script's own
|
|
# fallback validation to catch a broken pointer field.
|
|
echo ""
|
|
echo "--- catches a broken pointer field in a non-apm plugin's plugin.json ---"
|
|
FIXTURE5="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE5"' EXIT
|
|
mkdir -p "$FIXTURE5/.claude-plugin"
|
|
mkdir -p "$FIXTURE5/plugins/legacy/.claude-plugin"
|
|
cat > "$FIXTURE5/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "legacy", "source": "./plugins/legacy" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE5/plugins/legacy/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "legacy",
|
|
"skills": ["./skills/does-not-exist"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE5" > /dev/null 2>&1; then
|
|
fail "exited 0 for a non-apm plugin with a broken skills pointer -- expected exit 1"
|
|
else
|
|
pass "catches a broken skills pointer field in a non-apm (no .apm/) plugin.json"
|
|
fi
|
|
|
|
# --- 6. Non-.apm/ plugin with valid pointer fields still passes (no false positive) ---
|
|
echo ""
|
|
echo "--- a non-apm plugin with valid pointer fields still passes ---"
|
|
FIXTURE6="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE6"' EXIT
|
|
mkdir -p "$FIXTURE6/.claude-plugin"
|
|
mkdir -p "$FIXTURE6/plugins/legacy-ok/.claude-plugin"
|
|
mkdir -p "$FIXTURE6/plugins/legacy-ok/skills/real-skill"
|
|
cat > "$FIXTURE6/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "legacy-ok", "source": "./plugins/legacy-ok" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE6/plugins/legacy-ok/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "legacy-ok",
|
|
"skills": ["./skills/real-skill"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE6" > /dev/null 2>&1; then
|
|
pass "a non-apm plugin with a resolving skills pointer passes"
|
|
else
|
|
fail "exited non-zero for a non-apm plugin whose pointer fields all resolve"
|
|
fi
|
|
|
|
# --- 7. An .apm/ plugin with a broken pointer field is NOT caught here (delegated) ---
|
|
# Guards against the fallback path in finding #6 accidentally widening to also
|
|
# validate apm-native plugins, which would duplicate (and could disagree with)
|
|
# sync-plugin-content.sh --check's own drift detection.
|
|
echo ""
|
|
echo "--- an apm-native plugin's pointer fields are left to sync-plugin-content.sh --check ---"
|
|
FIXTURE7="$(mktemp -d)"
|
|
trap 'rm -rf "$FIXTURE7"' EXIT
|
|
mkdir -p "$FIXTURE7/.claude-plugin"
|
|
mkdir -p "$FIXTURE7/plugins/apm-plugin/.claude-plugin"
|
|
mkdir -p "$FIXTURE7/plugins/apm-plugin/.apm"
|
|
cat > "$FIXTURE7/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "apm-plugin", "source": "./plugins/apm-plugin" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE7/plugins/apm-plugin/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "apm-plugin",
|
|
"skills": ["./skills/does-not-exist"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE7" > /dev/null 2>&1; then
|
|
pass "an apm-native plugin (has .apm/) is not checked here, even with a broken pointer field"
|
|
else
|
|
fail "check-manifests.sh failed on an apm-native plugin -- pointer-field validation should be delegated, not duplicated"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|