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>
This commit is contained in:
2026-09-16 10:33:33 +00:00
parent 89b1c6fc85
commit 1ce596cdbe
2 changed files with 293 additions and 53 deletions

View File

@@ -188,9 +188,11 @@ 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" "$(cd "$F" && git rev-parse main)"
expect_pass "clean TO_REF passes while HEAD is unbumped" "$F" "$CLEAN"
echo ""
echo "--- 14. all-zeros delete sha no-ops ---"
@@ -247,6 +249,162 @@ F="$(mktemp -d)"; CLEANUP_DIRS+=("$F")
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 ]]