#!/usr/bin/env bash # The autofixing pre-commit hooks must leave apm's own output byte-identical. # # apm owns four tracked files, and `apm audit --ci` diffs them byte-for-byte # against a replayed install. If an autofixing hook rewrites any of them, the # pre-push gate reports drift on a file that has no git diff. This suite runs # the repo's real autofix hooks, with the real args and the real `exclude:` from # .pre-commit-config.yaml, over copies of those four files and asserts nothing # changes. A file the config excludes passes trivially; a file it leaves in scope # must survive the formatter untouched, which is what fails if `--no-sort-keys` # is ever dropped. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } # exit 77 (automake convention; run-tests.sh renders it as SKIPPED, and # --strict turns that into a setup error at the pre-push gate). command -v git > /dev/null 2>&1 || { echo "SKIP: git is required"; exit 77; } command -v pre-commit > /dev/null 2>&1 || { echo "SKIP: pre-commit is required to run pretty-format-json"; exit 77; } command -v python3 > /dev/null 2>&1 || { echo "SKIP: python3 is required to read .pre-commit-config.yaml"; exit 77; } python3 -c 'import yaml' > /dev/null 2>&1 || { echo "SKIP: PyYAML is required to read .pre-commit-config.yaml"; exit 77; } TOOL_OWNED=( .claude/settings.json .claude/apm-hooks.json .claude-plugin/marketplace.json apm.lock.yaml ) HOOK_IDS="pretty-format-json,end-of-file-fixer,trailing-whitespace" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT # Derive the fixture config from the real one so the args under test are the # repo's, not a copy that can drift. `stages:` is dropped so the hooks run under # a plain `pre-commit run`. if ! python3 - "$REPO_ROOT/.pre-commit-config.yaml" "$HOOK_IDS" "$WORK/pre-commit-config.yaml" << 'PY' import sys import yaml src, ids, dest = sys.argv[1], set(sys.argv[2].split(",")), sys.argv[3] cfg = yaml.safe_load(open(src)) repos = [] for repo in cfg["repos"]: hooks = [dict(h) for h in repo.get("hooks", []) if h["id"] in ids] if not hooks: continue for h in hooks: h.pop("stages", None) repos.append({"repo": repo["repo"], "rev": repo["rev"], "hooks": hooks}) found = {h["id"] for r in repos for h in r["hooks"]} missing = ids - found if missing: sys.exit("hooks not found in .pre-commit-config.yaml: " + ", ".join(sorted(missing))) yaml.safe_dump({"repos": repos}, open(dest, "w")) PY then echo "ERROR: could not derive the fixture config" exit 1 fi # pre-commit clones a hook repo once and caches it; without the cache this # would need the network, which is a setup problem, not a regression. HOOK_REPO="$(python3 -c 'import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))["repos"][0]["repo"])' "$WORK/pre-commit-config.yaml")" HOOK_REV="$(python3 -c 'import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))["repos"][0]["rev"])' "$WORK/pre-commit-config.yaml")" CACHE_DB="${PRE_COMMIT_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/pre-commit}/db.db" if ! python3 - "$CACHE_DB" "$HOOK_REPO" "$HOOK_REV" << 'PY' > /dev/null 2>&1 import sqlite3, sys db, repo, rev = sys.argv[1:4] row = sqlite3.connect(db).execute("select 1 from repos where repo=? and ref=?", (repo, rev)).fetchone() sys.exit(0 if row else 1) PY then echo "SKIP: $HOOK_REPO@$HOOK_REV is not in the pre-commit cache; run the hooks once with network access" exit 77 fi mkdir -p "$WORK/repo" cd "$WORK/repo" git init -q . git config user.email test@example.invalid git config user.name test cp "$WORK/pre-commit-config.yaml" .pre-commit-config.yaml for f in "${TOOL_OWNED[@]}"; do mkdir -p "$(dirname "$f")" cp "$REPO_ROOT/$f" "$f" done git add -A echo "" echo "--- autofix hooks leave apm-owned files byte-identical ---" RC=0 pre-commit run --all-files > "$WORK/hooks.out" 2>&1 || RC=$? for f in "${TOOL_OWNED[@]}"; do if cmp -s "$REPO_ROOT/$f" "$f"; then pass "$f is unchanged by $HOOK_IDS" else fail "$f was rewritten by an autofixing hook -- apm audit --ci would report drift" diff "$REPO_ROOT/$f" "$f" | sed 's/^/ /' | head -20 || true fi done if [[ "$RC" -eq 0 ]]; then pass "pre-commit exits 0 over the tool-owned files" else fail "pre-commit exited $RC over the tool-owned files" sed 's/^/ /' "$WORK/hooks.out" fi echo "" echo "Results: $PASS passed, $FAIL failed" [[ "$FAIL" -eq 0 ]]