Files
holocron/tests/test-skill-version-bump.sh
Defame1297 614a0d5efa fix(gates): read leading-whitespace frontmatter in check-skill-version-bump
read_version required --- at byte 0 while skill-size-check accepts
leading blank lines, so a file one gate passed the other reported as
unversioned, and an unversioned merge-base side let an unbumped change
through. Match FRONTMATTER_RE, add case 39, and describe the main-tip
check and fail-closed cases in the hook entry.

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

594 lines
25 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 docs/spec/gates.md, Tests).
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. pushed version must also exceed 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_fail "bump over the merge-base fails when main's tip is higher" \
"alpha: 1\.0\.5 -> 1\.0\.1 \(not above main tip\)" "$F"
OUT="$(run_check "$F" || true)"
if grep -q "not above merge-base" <<< "$OUT"; then
fail "merge-base reported as failed although 1.0.1 > 1.0.0: $OUT"
else
pass "only the baseline actually failed is named"
fi
write_skill "$F" demo alpha 'version: "1.0.6"' "branch change 2"; commit "$F"
expect_pass "bump above both the merge-base and main's tip passes" "$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"
expect_fail "the report names alpha and the versions read from origin/main's merge-base" \
"demo/\.apm/skills/alpha: 1\.0\.1 -> 1\.0\.1 \(not above merge-base\)" "$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" \
"origin/main does not resolve and HEAD is already contained in local 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, bounded, and has no leading zeros ---"
for v in 'version: "1.0.1"' 'version: "1.0.1"' 'version: "1.0.9999999999"' \
'version: "99999999999999999999.0.0"' 'version: "1.0.08"' 'version: "01.0.1"' \
'version: "1.00.1"'; 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"
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.10"' "new body"; commit "$F"
expect_pass "a zero inside a part (1.0.10) is not a leading zero" "$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 objects as PRE_COMMIT_TO_REF ---"
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"
# Peeling is what makes the local-main fallback's "pushed commit is the
# merge-base" test see through a tag: compared unpeeled, the tag's own sha
# never equals the merge-base and the empty diff would pass.
(cd "$F" && git tag -a on-main -m "tag" main)
expect_fail "a tag on local main's commit fails closed like the commit itself" \
"origin/main does not resolve and [0-9a-f]+ is already contained in local main" \
"$F" "$(cd "$F" && git rev-parse on-main)"
(cd "$F" && git tag -a tree-tag -m "tag" "main^{tree}")
expect_fail "a tag on a tree fails closed" "pushed ref [0-9a-f]+ does not resolve to a commit" \
"$F" "$(cd "$F" && git rev-parse tree-tag)"
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 "--- 30. identical bump already merged to main fails ---"
# Branches A and B both bump alpha 1.0.0 -> 1.0.1 with different content. The
# bumps do not conflict at merge, so without the tip rule main would ship two
# changes under one version.
F="$(make_fixture)"
(cd "$F" && git checkout -q -b branch-a main)
write_skill "$F" demo alpha 'version: "1.0.1"' "change A"; commit "$F"
(cd "$F" && git checkout -q main && git merge -q --no-ff -m "merge A" branch-a \
&& git update-ref refs/remotes/origin/main main && git checkout -q feature)
write_skill "$F" demo alpha 'version: "1.0.1"' "change B"; commit "$F"
expect_fail "B's 1.0.1 fails against A's 1.0.1 on origin/main" \
"alpha: 1\.0\.1 -> 1\.0\.1 \(not above origin/main tip\)" "$F"
write_skill "$F" demo alpha 'version: "1.0.2"' "change B 2"; commit "$F"
expect_pass "B at 1.0.2 passes" "$F"
echo ""
echo "--- 31. skill deleted on main's tip: only the merge-base rule applies ---"
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
rm -rf "$F/plugins/demo/.apm/skills/alpha"; commit "$F" "drop alpha on main"
(cd "$F" && git checkout -q feature)
write_skill "$F" demo alpha 'version: "1.0.1"' "branch change"; commit "$F"
expect_pass "bump over the merge-base passes when main's tip lacks the skill" "$F"
(cd "$F" && git checkout -q -b unbumped main~1)
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
expect_fail "unbumped change still fails against the merge-base" \
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
echo ""
echo "--- 32. UTF-8 BOM before the frontmatter is parsed ---"
# bom <repo> <skill>: prefix that skill's SKILL.md with a UTF-8 byte-order mark.
bom() {
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
{ printf '\xef\xbb\xbf'; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
}
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.0"' "bom body"; bom "$F" alpha; commit "$F"
if [[ "$(head -c 3 "$F/plugins/demo/.apm/skills/alpha/SKILL.md" | od -An -tx1 | tr -d ' ')" == "efbbbf" ]]; then
pass "fixture check: SKILL.md starts with a BOM"
else
fail "fixture check: SKILL.md has no BOM"
fi
expect_fail "unbumped BOM skill reports its parsed version, not a missing one" \
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
write_skill "$F" demo alpha 'version: "1.0.1"' "bom body 2"; bom "$F" alpha; commit "$F"
expect_pass "bumped BOM skill passes" "$F"
echo ""
echo "--- 33. a file moved from one skill to another flags both ---"
# --no-renames: with rename detection, --name-only lists only the new path and
# alpha would lose a file without anyone noticing.
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
printf 'ref line %s\n' 1 2 3 4 5 > "$F/plugins/demo/.apm/skills/alpha/references/x.md"
commit "$F" "alpha reference"
(cd "$F" && git checkout -q feature && git merge -q main)
mkdir -p "$F/plugins/demo/.apm/skills/beta/references"
(cd "$F" && git mv plugins/demo/.apm/skills/alpha/references/x.md plugins/demo/.apm/skills/beta/references/x.md)
write_skill "$F" demo beta "version: 1.0.10"; commit "$F"
expect_fail "alpha is flagged although only beta was bumped" \
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
echo ""
echo "--- 34. comparison is ordered major first ---"
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
write_skill "$F" demo alpha 'version: "2.0.0"'; commit "$F" "alpha 2.0.0"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo alpha 'version: "1.9.0"' "new body"; commit "$F"
expect_fail "2.0.0 -> 1.9.0 fails although minor rose" "alpha: 2\.0\.0 -> 1\.9\.0" "$F"
echo ""
echo "--- 35. unresolvable PRE_COMMIT_TO_REF fails closed ---"
F="$(make_fixture)"
expect_fail "a sha absent from the repo fails" \
"pushed ref 1234567890abcdef1234567890abcdef12345678 does not resolve to a commit" \
"$F" "1234567890abcdef1234567890abcdef12345678"
echo ""
echo "--- 36. a SKILL.md git cannot read fails closed ---"
# drop_blob <repo> <rev:path>: delete that blob's loose object, as a corrupt or
# partial clone would lack it. The tree still names the file.
drop_blob() {
local sha
sha="$(cd "$1" && git rev-parse "$2")"
rm -f "$1/.git/objects/${sha:0:2}/${sha:2}"
}
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
drop_blob "$F" "HEAD:plugins/demo/.apm/skills/alpha/SKILL.md"
OUT="$(run_check "$F")" && RC=0 || RC=$?
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version from [0-9a-f]*:plugins/demo/.apm/skills/alpha/SKILL.md" <<< "$OUT" \
&& grep -q "fatal: bad object" <<< "$OUT" && ! grep -qE "missing or not|SKILL\.md missing" <<< "$OUT"; then
pass "an unreadable pushed SKILL.md is a read failure carrying git's error"
else
fail "unreadable pushed SKILL.md misreported (rc=$RC): $OUT"
fi
F="$(make_fixture)"
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
drop_blob "$F" "main:plugins/demo/.apm/skills/alpha/SKILL.md"
OUT="$(run_check "$F")" && RC=0 || RC=$?
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version" <<< "$OUT"; then
pass "an unreadable merge-base SKILL.md fails closed instead of exempting the skill"
else
fail "unreadable merge-base SKILL.md not caught (rc=$RC): $OUT"
fi
echo ""
echo "--- 37. a mode-only change counts as a change ---"
F="$(make_fixture)"
(cd "$F" && git config core.fileMode true)
chmod +x "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; commit "$F"
if [[ "$(cd "$F" && git diff --summary main HEAD)" == *"mode change 100644 => 100755"* ]]; then
pass "fixture check: the commit changes only the file mode"
else
fail "fixture check: no mode change recorded"
fi
expect_fail "chmod +x without a bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
echo ""
echo "--- 38. skill directory replaced by a symlink ---"
# Current behaviour, pinned: the path is no longer a tree at the pushed commit,
# so the skill is exempt as deleted. apm drops symlinks under .apm/ (ADR-0017),
# so readers do lose the skill.
F="$(make_fixture)"
rm -rf "$F/plugins/demo/.apm/skills/alpha"
ln -s beta "$F/plugins/demo/.apm/skills/alpha"; commit "$F"
if [[ "$(cd "$F" && git ls-tree HEAD plugins/demo/.apm/skills/alpha)" == 120000* ]]; then
pass "fixture check: alpha is committed as a symlink"
else
fail "fixture check: alpha is not a symlink in the commit"
fi
expect_pass "a skill replaced by a symlink is exempt as deleted" "$F"
echo ""
echo "--- 39. leading blank lines before the frontmatter are parsed ---"
# skill-size-check's FRONTMATTER_RE accepts whitespace before the opening
# `---`; this gate must read the same shape. Otherwise the pushed side reports
# "missing" for a file the commit hook accepted, and a baseline shaped this way
# reads as "no valid version", which lets any version pass.
# blank_lead <repo> <skill>: prefix that skill's SKILL.md with blank lines.
blank_lead() {
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
{ printf '\n \t\n'; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
}
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
blank_lead "$F" alpha; commit "$F" "alpha leading blank"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo alpha 'version: "1.0.0"' "lead body"; blank_lead "$F" alpha; commit "$F"
expect_fail "unbumped skill with leading blank lines is held to its baseline version" \
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
write_skill "$F" demo alpha 'version: "1.0.1"' "lead body 2"; blank_lead "$F" alpha; commit "$F"
expect_pass "bumped skill with leading blank lines passes" "$F"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]