#!/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 [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 [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 [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" < "$SHIM_ROOT/crash/python3" <&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 : 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 : 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 : 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 : 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 ]]