Files
holocron/tests/test-skill-version-bump.sh
Defame1297 1ce596cdbe fix(gates): close the review findings in check-skill-version-bump
- Read changed paths NUL-delimited so non-ASCII paths are no longer
  silently skipped.
- Fail closed when only local main resolves and the pushed commit is
  the merge-base, instead of passing on an empty diff.
- Accept ASCII-only versions with at most nine digits per part.
- Check for python3/PyYAML up front, and report read failures as such
  rather than as a missing version; name a missing SKILL.md.
- Document that pre-commit gates only the first ref of a multi-ref push.

Tests grow to 29 cases covering each fix plus annotated tags, CRLF
frontmatter, unrelated histories and pushing main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:33:33 +00:00

411 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$REPO_ROOT/scripts/check-skill-version-bump.sh"
PASS=0
FAIL=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# The gate reads versions with python3 + PyYAML, the same hard requirement
# skill-size-check carries. Without them this suite cannot run: exit 77 so
# run-tests reports SKIPPED (and --strict fails the push).
if ! python3 -c 'import yaml' 2>/dev/null; then
echo "SKIP: python3 with PyYAML is required" >&2
exit 77
fi
# Output assertions use here-strings, never `echo | grep -q` (pipefail race;
# see tests/test-check-release-needed.sh).
CLEANUP_DIRS=()
trap 'rm -rf ${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}' EXIT
# write_skill <repo> <plugin> <skill> <version-line|""> [body]
# An empty version line writes a SKILL.md with no metadata block at all.
write_skill() {
local repo="$1" plugin="$2" skill="$3" vline="$4" body="${5:-body}"
local d="$repo/plugins/$plugin/.apm/skills/$skill"
mkdir -p "$d"
{
echo "---"
echo "name: $skill"
echo "description: Use when testing."
if [[ -n "$vline" ]]; then
echo "metadata:"
echo " $vline"
fi
echo "---"
echo "$body"
} > "$d/SKILL.md"
}
commit() { (cd "$1" && git add -A && git commit -q -m "${2:-change}"); }
# A fixture with skills alpha (1.0.0) and beta (1.0.9) on main, then checked out
# onto a feature branch.
make_fixture() {
local dir
dir="$(mktemp -d)"
CLEANUP_DIRS+=("$dir")
(cd "$dir" && git init -q -b main && git config user.email t@t.t && git config user.name t)
write_skill "$dir" demo alpha 'version: "1.0.0"'
write_skill "$dir" demo beta "version: 1.0.9"
commit "$dir" initial
(cd "$dir" && git checkout -q -b feature)
echo "$dir"
}
# Every PRE_COMMIT_* input comes from here: a value inherited from the pre-push
# hook running this suite names a sha of the real repo, not the fixture.
run_check() {
local dir="$1"
if [[ $# -ge 2 ]]; then
(cd "$dir" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_REMOTE_BRANCH \
&& PRE_COMMIT_TO_REF="$2" bash "$SCRIPT" 2>&1)
else
(cd "$dir" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
&& bash "$SCRIPT" 2>&1)
fi
}
# expect_pass <desc> <dir> [to-ref]
expect_pass() {
local desc="$1"; shift
local out
if out="$(run_check "$@")" && [[ -z "$out" ]]; then
pass "$desc"
else
fail "$desc — expected silent exit 0, got: $out"
fi
}
# expect_fail <desc> <pattern> <dir> [to-ref]
expect_fail() {
local desc="$1" pattern="$2"; shift 2
local out
if out="$(run_check "$@")"; then
fail "$desc — expected non-zero exit, got 0"
elif grep -qE "$pattern" <<< "$out"; then
pass "$desc"
else
fail "$desc — output did not match /$pattern/: $out"
fi
}
echo ""
echo "--- 1. unchanged skill passes ---"
F="$(make_fixture)"
echo "unrelated" > "$F/README.md"; commit "$F"
expect_pass "no skill change passes silently" "$F"
echo ""
echo "--- 2. changed without bump fails ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "new body"; commit "$F"
expect_fail "unbumped change fails and names baseline and current" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
echo ""
echo "--- 3. changed with patch / minor / major bump passes ---"
for v in 1.0.1 1.1.0 2.0.0; do
F="$(make_fixture)"
write_skill "$F" demo alpha "version: \"$v\"" "new body"; commit "$F"
expect_pass "bump to $v passes" "$F"
done
echo ""
echo "--- 4. version decreased fails ---"
F="$(make_fixture)"
write_skill "$F" demo beta "version: 1.0.8" "new body"; commit "$F"
expect_fail "decrease fails" "beta: 1\.0\.9 -> 1\.0\.8" "$F"
echo ""
echo "--- 5. change to a non-SKILL.md file still counts ---"
F="$(make_fixture)"
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/x.md"; commit "$F"
expect_fail "references/ change without bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
echo ""
echo "--- 6. tests/-only change passes without bump ---"
F="$(make_fixture)"
mkdir -p "$F/plugins/demo/.apm/skills/alpha/tests"
echo "t" > "$F/plugins/demo/.apm/skills/alpha/tests/test-x.sh"; commit "$F"
expect_pass "tests/-only change is exempt" "$F"
echo ""
echo "--- 7. new skill exempt ---"
F="$(make_fixture)"
write_skill "$F" demo gamma 'version: "0.1.0"'; commit "$F"
expect_pass "new skill passes" "$F"
echo ""
echo "--- 8. deleted skill exempt ---"
F="$(make_fixture)"
rm -rf "$F/plugins/demo/.apm/skills/alpha"; commit "$F"
expect_pass "deleted skill passes" "$F"
echo ""
echo "--- 9. renamed skill exempt ---"
F="$(make_fixture)"
(cd "$F" && git mv plugins/demo/.apm/skills/alpha plugins/demo/.apm/skills/alpha2)
commit "$F"
expect_pass "renamed skill passes (old absent at pushed, new absent at baseline)" "$F"
echo ""
echo "--- 10. missing version on changed skill fails ---"
F="$(make_fixture)"
write_skill "$F" demo alpha "" "new body"; commit "$F"
expect_fail "missing version fails" "alpha: metadata\.version missing" "$F"
echo ""
echo "--- 11. malformed / prerelease version fails ---"
for v in 'version: 1.1' 'version: "1.0.1-rc1"'; do
F="$(make_fixture)"
write_skill "$F" demo alpha "$v" "new body"; commit "$F"
expect_fail "'$v' is rejected like skill-size-check rejects it" "alpha: metadata\.version missing or not" "$F"
done
echo ""
echo "--- 12. multiple offenders all reported ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "x"
write_skill "$F" demo beta "version: 1.0.9" "x"
commit "$F"
OUT="$(run_check "$F" || true)"
if grep -q "alpha: 1.0.0 -> 1.0.0" <<< "$OUT" && grep -q "beta: 1.0.9 -> 1.0.9" <<< "$OUT" \
&& grep -q "bump PATCH at minimum" <<< "$OUT"; then
pass "both offenders and the fix are reported"
else
fail "not every offender reported: $OUT"
fi
echo ""
echo "--- 13. PRE_COMMIT_TO_REF respected over HEAD ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
BAD="$(cd "$F" && git rev-parse HEAD)"
(cd "$F" && git checkout -q -b clean main)
echo "unrelated" > "$F/README.md"; commit "$F"
CLEAN="$(cd "$F" && git rev-parse HEAD)"
expect_fail "unbumped TO_REF fails while HEAD is clean" "alpha: 1\.0\.0 -> 1\.0\.0" "$F" "$BAD"
(cd "$F" && git checkout -q "$BAD")
expect_pass "clean TO_REF passes while HEAD is unbumped" "$F" "$CLEAN"
echo ""
echo "--- 14. all-zeros delete sha no-ops ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
expect_pass "40-zero sha exits 0" "$F" "0000000000000000000000000000000000000000"
expect_pass "64-zero sha exits 0" "$F" "$(printf '0%.0s' {1..64})"
echo ""
echo "--- 15. bin plugin covered ---"
F="$(make_fixture)"
write_skill "$F" bin tool 'version: "1.0.0"'
(cd "$F" && git checkout -q main); commit "$F" "add bin skill"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" bin tool 'version: "1.0.0"' "changed"; commit "$F"
expect_fail "bin skill change without bump fails" "plugins/bin/\.apm/skills/tool: 1\.0\.0 -> 1\.0\.0" "$F"
echo ""
echo "--- 16. multi-digit semver compare ---"
F="$(make_fixture)"
write_skill "$F" demo beta "version: 1.0.10" "x"; commit "$F"
expect_pass "1.0.10 > 1.0.9 passes" "$F"
echo ""
echo "--- 17. baseline is the merge-base, not main's tip ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.1"' "branch change"; commit "$F"
(cd "$F" && git checkout -q main)
write_skill "$F" demo alpha 'version: "1.0.5"' "main moved on"; commit "$F"
(cd "$F" && git checkout -q feature)
expect_pass "bump over the merge-base passes even though main is ahead" "$F"
echo ""
echo "--- 18. origin/main preferred over local main ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.1"' "x"; commit "$F"
# A stale local main pointing at the feature tip would make the diff empty;
# origin/main at the original commit must win and still see the change.
(cd "$F" && git update-ref refs/remotes/origin/main main && git branch -f main feature)
write_skill "$F" demo alpha 'version: "1.0.1"' "y"; commit "$F"
if OUT="$(run_check "$F")" && [[ -z "$OUT" ]]; then
pass "origin/main used as baseline (1.0.0 -> 1.0.1 counted as a bump)"
else
fail "unexpected output: $OUT"
fi
(cd "$F" && git update-ref refs/remotes/origin/main feature~1)
expect_fail "origin/main at the bumped commit flags the further unbumped change" \
"merge-base with origin/main" "$F"
echo ""
echo "--- 19. no main ref fails closed ---"
F="$(mktemp -d)"; CLEANUP_DIRS+=("$F")
(cd "$F" && git init -q -b trunk && git config user.email t@t.t && git config user.name t)
write_skill "$F" demo alpha 'version: "1.0.0"'; commit "$F"
expect_fail "missing main fails with a clear message" "neither origin/main nor main resolves" "$F"
echo ""
echo "--- 20. non-ASCII paths are not hidden by core.quotePath ---"
F="$(make_fixture)"
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/résumé.md"; commit "$F"
expect_fail "non-ASCII file under references/ without bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
write_skill "$F" demo "café" 'version: "2.0.0"'; commit "$F" "add café"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo "café" 'version: "2.0.0"' "changed"; commit "$F"
expect_fail "non-ASCII skill dir without bump fails" "skills/café: 2\.0\.0 -> 2\.0\.0" "$F"
write_skill "$F" demo "café" 'version: "2.0.1"' "changed again"; commit "$F"
expect_pass "non-ASCII skill dir with bump passes" "$F"
echo ""
echo "--- 21. local main fallback: pushed commit already in main fails closed ---"
F="$(make_fixture)"
MAIN_SHA="$(cd "$F" && git rev-parse main)"
expect_fail "pushing main's sha without origin/main fails" \
"origin/main does not resolve.*already contained in local main" "$F" "$MAIN_SHA"
(cd "$F" && git checkout -q main)
expect_fail "HEAD on main without origin/main fails" "Fix: git fetch origin main" "$F"
echo ""
echo "--- 22. pushing main itself with origin/main present ---"
F="$(make_fixture)"
(cd "$F" && git checkout -q main && git update-ref refs/remotes/origin/main main)
expect_pass "main equal to origin/main passes (nothing changed vs main)" "$F"
write_skill "$F" demo alpha 'version: "1.0.1"' "x"; commit "$F"
expect_pass "main ahead of origin/main with a bump passes" "$F"
write_skill "$F" demo beta "version: 1.0.9" "x"; commit "$F"
expect_fail "main ahead of origin/main without a bump fails" "beta: 1\.0\.9 -> 1\.0\.9" "$F"
(cd "$F" && git update-ref refs/remotes/origin/main main)
expect_pass "already-merged content (merge-base == pushed) passes against origin/main" "$F" \
"$(cd "$F" && git rev-parse main~1)"
echo ""
echo "--- 23. version shape is ASCII-only and bounded ---"
for v in 'version: "1.0.1"' 'version: "1.0.1"' 'version: "1.0.9999999999"' \
'version: "99999999999999999999.0.0"'; do
F="$(make_fixture)"
write_skill "$F" demo alpha "$v" "new body"; commit "$F"
OUT="$(run_check "$F" || true)"
if grep -q "alpha: metadata\.version missing or not" <<< "$OUT" \
&& ! grep -qiE "integer|syntax error|value too great" <<< "$OUT"; then
pass "'$v' is rejected as invalid without a bash arithmetic error"
else
fail "'$v' not cleanly rejected: $OUT"
fi
done
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.999999999"' "new body"; commit "$F"
expect_pass "nine-digit part is accepted and compared" "$F"
echo ""
echo "--- 24. python3 / PyYAML failures are never reported as a missing version ---"
REAL_PYTHON="$(command -v python3)"
SHIM_ROOT="$(mktemp -d)"; CLEANUP_DIRS+=("$SHIM_ROOT")
mkdir -p "$SHIM_ROOT/shadow" "$SHIM_ROOT/noyaml" "$SHIM_ROOT/crash"
printf 'raise ImportError("PyYAML deliberately unavailable in this fixture")\n' \
> "$SHIM_ROOT/shadow/yaml.py"
cat > "$SHIM_ROOT/noyaml/python3" <<EOF
#!/bin/sh
PYTHONPATH="$SHIM_ROOT/shadow\${PYTHONPATH:+:\$PYTHONPATH}" exec "$REAL_PYTHON" "\$@"
EOF
# Passes the up-front import probe, crashes on the real read.
cat > "$SHIM_ROOT/crash/python3" <<EOF
#!/bin/sh
[ "\$1" = "-c" ] && [ "\$2" = "import yaml" ] && exec "$REAL_PYTHON" "\$@"
echo "Traceback: simulated interpreter failure" >&2
exit 1
EOF
chmod +x "$SHIM_ROOT/noyaml/python3" "$SHIM_ROOT/crash/python3"
if PATH="$SHIM_ROOT/noyaml:$PATH" python3 -c 'import yaml' 2>/dev/null; then
fail "fixture check: the no-PyYAML shim still imports yaml — the next assertion would be vacuous"
else
pass "fixture check: the no-PyYAML shim makes 'import yaml' fail"
fi
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
OUT="$(cd "$F" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
&& PATH="$SHIM_ROOT/noyaml:$PATH" bash "$SCRIPT" 2>&1)" && RC=0 || RC=$?
if [[ $RC -ne 0 ]] && grep -q "PyYAML is required" <<< "$OUT" && grep -q "Fix: python3 -m pip install PyYAML" <<< "$OUT" \
&& ! grep -q "Traceback" <<< "$OUT"; then
pass "missing PyYAML fails with FAIL + Fix, no traceback"
else
fail "missing PyYAML not reported cleanly (rc=$RC): $OUT"
fi
OUT="$(cd "$F" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
&& PATH="$SHIM_ROOT/crash:$PATH" bash "$SCRIPT" 2>&1)" && RC=0 || RC=$?
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version" <<< "$OUT" \
&& ! grep -q "missing or not" <<< "$OUT"; then
pass "python3 crash during the read is a read failure, not a missing version"
else
fail "python3 crash misreported (rc=$RC): $OUT"
fi
echo ""
echo "--- 25. SKILL.md deleted but skill dir kept ---"
F="$(make_fixture)"
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/x.md"
rm "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; commit "$F"
expect_fail "missing SKILL.md is reported as such" "alpha: SKILL\.md missing at HEAD \(baseline: 1\.0\.0\)" "$F"
echo ""
echo "--- 26. baseline without a valid version accepts any valid version ---"
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
write_skill "$F" demo legacy ""; commit "$F" "add legacy skill"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo legacy 'version: "0.0.1"' "changed"; commit "$F"
expect_pass "invalid baseline + valid current passes" "$F"
echo ""
echo "--- 27. no merge-base (unrelated histories) fails closed ---"
F="$(make_fixture)"
(cd "$F" && git checkout -q --orphan unrelated && git rm -rq --cached . && rm -rf plugins)
write_skill "$F" demo alpha 'version: "1.0.0"' "orphan"; commit "$F" "orphan root"
expect_fail "unrelated history fails at the merge-base check" "no merge-base between main and HEAD" "$F"
echo ""
echo "--- 28. annotated tag push is peeled to its commit ---"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
(cd "$F" && git tag -a v9 -m "tag" && git checkout -q main)
TAG_OBJ="$(cd "$F" && git rev-parse v9)"
if [[ "$(cd "$F" && git cat-file -t "$TAG_OBJ")" == "tag" ]]; then
pass "fixture check: TO_REF is a tag object, not a commit"
else
fail "fixture check: v9 is not an annotated tag object"
fi
expect_fail "unbumped change behind an annotated tag fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F" "$TAG_OBJ"
echo ""
echo "--- 29. CRLF frontmatter is parsed ---"
# crlf <repo> <skill>: rewrite that skill's SKILL.md with CRLF line endings.
crlf() {
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
sed 's/$/\r/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
}
F="$(make_fixture)"
(cd "$F" && git config core.autocrlf false && git checkout -q main)
crlf "$F" alpha; commit "$F" "alpha to CRLF"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo alpha 'version: "1.0.0"' "crlf body"; crlf "$F" alpha; commit "$F"
if grep -q $'\r' "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; then
pass "fixture check: SKILL.md carries CRLF"
else
fail "fixture check: SKILL.md has no CRLF"
fi
expect_fail "unbumped CRLF skill reports both parsed versions" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
write_skill "$F" demo alpha 'version: "1.0.1"' "crlf body 2"; crlf "$F" alpha; commit "$F"
expect_pass "bumped CRLF skill passes" "$F"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]