#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT="$REPO_ROOT/scripts/setup-hooks.sh" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } # Helper: make a bare git repo with no hooks yet make_repo() { local dir dir="$(mktemp -d)" git -C "$dir" init -q echo "$dir" } # Fake binaries for tools we don't want to install-check during tests FAKE_BIN="$(mktemp -d)" trap 'rm -rf "$FAKE_BIN"' EXIT for tool in shellcheck jq yq; do printf '#!/bin/sh\necho "fake %s"\n' "$tool" > "$FAKE_BIN/$tool" chmod +x "$FAKE_BIN/$tool" done export PATH="$FAKE_BIN:$PATH" # --- 1. Rejects non-git directory --- echo "" echo "--- rejects non-git directory ---" NON_GIT="$(mktemp -d)" trap 'rm -rf "$NON_GIT"' EXIT if bash "$SCRIPT" "$NON_GIT" > /dev/null 2>&1; then fail "exited 0 for non-git directory — expected exit 1" else pass "exits non-zero for non-git directory" fi # --- 2. Creates commit-msg hook --- echo "" echo "--- creates commit-msg hook ---" REPO="$(make_repo)" trap 'rm -rf "$REPO"' EXIT bash "$SCRIPT" "$REPO" > /dev/null 2>&1 HOOK="$REPO/.git/hooks/commit-msg" if [[ -f "$HOOK" ]]; then pass "commit-msg hook file created" else fail "commit-msg hook not created" fi if [[ -x "$HOOK" ]]; then pass "commit-msg hook is executable" else fail "commit-msg hook is not executable" fi if grep -q "# managed by setup-hooks.sh" "$HOOK"; then pass "commit-msg hook contains idempotency marker" else fail "commit-msg hook missing idempotency marker" fi # --- 3. commit-msg hook validates conventional commits --- echo "" echo "--- commit-msg hook: valid message passes ---" REPO2="$(make_repo)" trap 'rm -rf "$REPO2"' EXIT bash "$SCRIPT" "$REPO2" > /dev/null 2>&1 HOOK2="$REPO2/.git/hooks/commit-msg" TMPFILE="$(mktemp)" trap 'rm -f "$TMPFILE"' EXIT for valid_msg in "feat: add validation" "fix(core): correct path resolution" "chore!: drop python dep" "docs: update readme" "refactor(hooks): extract marker logic"; do echo "$valid_msg" > "$TMPFILE" if bash "$HOOK2" "$TMPFILE" > /dev/null 2>&1; then pass "commit-msg hook accepts: $valid_msg" else fail "commit-msg hook wrongly rejected: $valid_msg" fi done echo "" echo "--- commit-msg hook: invalid message is rejected ---" for invalid_msg in "added some stuff" "WIP" "Fix the thing" "FEAT: bad case" "feat bad colon"; do echo "$invalid_msg" > "$TMPFILE" if bash "$HOOK2" "$TMPFILE" > /dev/null 2>&1; then fail "commit-msg hook wrongly accepted: $invalid_msg" else pass "commit-msg hook rejects: $invalid_msg" fi done # --- 4. Appends pre-commit validation block --- echo "" echo "--- appends validation block to pre-commit hook ---" REPO3="$(make_repo)" trap 'rm -rf "$REPO3"' EXIT bash "$SCRIPT" "$REPO3" > /dev/null 2>&1 PRE_COMMIT="$REPO3/.git/hooks/pre-commit" if [[ -f "$PRE_COMMIT" ]]; then pass "pre-commit hook created" else fail "pre-commit hook not created" fi if grep -q "shellcheck" "$PRE_COMMIT"; then pass "pre-commit hook contains shellcheck" else fail "pre-commit hook missing shellcheck" fi if grep -q "jq" "$PRE_COMMIT"; then pass "pre-commit hook contains jq" else fail "pre-commit hook missing jq" fi if grep -q "SKILL.md" "$PRE_COMMIT"; then pass "pre-commit hook contains SKILL.md frontmatter check" else fail "pre-commit hook missing SKILL.md frontmatter check" fi # --- 5. Creates pre-push hook --- echo "" echo "--- creates pre-push hook ---" REPO4="$(make_repo)" trap 'rm -rf "$REPO4"' EXIT bash "$SCRIPT" "$REPO4" > /dev/null 2>&1 PUSH_HOOK="$REPO4/.git/hooks/pre-push" if [[ -f "$PUSH_HOOK" ]]; then pass "pre-push hook created" else fail "pre-push hook not created" fi if [[ -x "$PUSH_HOOK" ]]; then pass "pre-push hook is executable" else fail "pre-push hook is not executable" fi if grep -q "check-manifests" "$PUSH_HOOK"; then pass "pre-push hook calls check-manifests.sh" else fail "pre-push hook missing check-manifests.sh call" fi # --- 6. Idempotent: second run replaces each block exactly once --- echo "" echo "--- idempotent: second run does not duplicate blocks ---" REPO5="$(make_repo)" trap 'rm -rf "$REPO5"' EXIT bash "$SCRIPT" "$REPO5" > /dev/null 2>&1 bash "$SCRIPT" "$REPO5" > /dev/null 2>&1 bash "$SCRIPT" "$REPO5" > /dev/null 2>&1 for hook_file in "$REPO5/.git/hooks/commit-msg" "$REPO5/.git/hooks/pre-commit" "$REPO5/.git/hooks/pre-push"; do count=$(grep -c "# managed by setup-hooks.sh" "$hook_file" || true) hook_name="$(basename "$hook_file")" if [[ "$count" -eq 1 ]]; then pass "idempotent: $hook_name marker appears exactly once after 3 runs" else fail "idempotent: $hook_name marker appears $count times — block duplicated" fi done # --- 7. Hooks contain graceful degradation paths for missing tools --- echo "" echo "--- hooks contain graceful degradation for missing tools ---" REPO6="$(make_repo)" trap 'rm -rf "$REPO6"' EXIT bash "$SCRIPT" "$REPO6" > /dev/null 2>&1 PRE_COMMIT6="$REPO6/.git/hooks/pre-commit" for tool in shellcheck jq yq; do if grep -q "Warning:.*$tool\|$tool.*not installed\|$tool.*skipped" "$PRE_COMMIT6"; then pass "pre-commit hook has graceful degradation path for missing: $tool" else fail "pre-commit hook missing graceful degradation for: $tool" fi done echo "" echo "--- setup always exits 0 (no hard dependency on optional tools) ---" REPO7="$(make_repo)" trap 'rm -rf "$REPO7"' EXIT if bash "$SCRIPT" "$REPO7" > /dev/null 2>&1; then pass "setup exits 0 when all optional tools are present" else fail "setup exited non-zero unexpectedly" fi echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]