#!/usr/bin/env bash # Integration test for .pre-commit-hooks.yaml as an EXTERNAL hook repo — the # contract ADR-0014 exists to provide, and the one thing running pre-commit # inside this repo can never exercise: `repo: local` makes pre-commit's clone # prefix equal to the consuming repo's root, so a hook entry that only works # because those two coincide passes here and hard-fails everywhere else. # (It did: every argument after entry[0] resolves against the CONSUMING repo, # so a `--config plugins/.../.vale.ini` argument gave external consumers # `E100 [--config] Runtime error ... does not exist`, exit 2, on both Vale hooks.) # # The hook repo is built from the WORKING TREE, not from HEAD, so an uncommitted # change to the manifest or the wrapper is what gets tested. 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)); } for bin in pre-commit vale git; do if ! command -v "$bin" &>/dev/null; then echo "SKIP: $bin is not installed — cannot stand up a consumer repo" exit 77 fi done WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT HOOK_REPO="$WORK/hookrepo" CONSUMER="$WORK/consumer" export PRE_COMMIT_HOME="$WORK/pc-home" mkdir -p "$HOOK_REPO/plugins/kyberforge/skills" "$HOOK_REPO/scripts" cp "$REPO_ROOT/.pre-commit-hooks.yaml" "$HOOK_REPO/" cp "$REPO_ROOT/scripts/skill-size-check.sh" "$HOOK_REPO/scripts/" for skill in skill-audit agent-audit; do mkdir -p "$HOOK_REPO/plugins/kyberforge/skills/$skill" cp -R "$REPO_ROOT/plugins/kyberforge/skills/$skill/scripts" \ "$REPO_ROOT/plugins/kyberforge/skills/$skill/assets" \ "$HOOK_REPO/plugins/kyberforge/skills/$skill/" done git -C "$HOOK_REPO" init -q git -C "$HOOK_REPO" add -A git -C "$HOOK_REPO" -c user.email=test@example.invalid -c user.name=test commit -qm "hook repo" HOOK_REV="$(git -C "$HOOK_REPO" rev-parse HEAD)" # Every hook scopes by filename, so the consumer needs one file of each shape: # a hook with nothing to match reports `Skipped` and proves nothing. All three # hooks .pre-commit-hooks.yaml ships are registered — an unregistered one would # let a regression (a lost `100755` bit, a bad entry path) reach every external # consumer while this repo's own `repo: local` runs stayed green. mkdir -p "$CONSUMER/skills/demo" "$CONSUMER/agents" git -C "$CONSUMER" init -q cat > "$CONSUMER/.pre-commit-config.yaml" < "$CONSUMER/skills/demo/SKILL.md" < Use when the caller wants a demonstration skill $skill_body across two physical lines of one folded block scalar. --- Body. EOF cat > "$CONSUMER/agents/demo.md" < Use when the caller wants a demonstration agent $agent_body across two physical lines of one folded block scalar. --- Body. EOF git -C "$CONSUMER" add -A } # Vale prints each linted path as its own header line with that file's alerts # indented beneath it, so an alert belongs to the nearest preceding path line. # Reads a hook log on stdin and prints only the alert lines filed under `$1`. # The `sed` strips vale's ANSI colouring, which it emits into pre-commit's pipe # too, so the header lines compare as plain paths. alerts_for() { sed $'s/\033\\[[0-9;]*m//g' | awk -v want="$1" ' /^[^[:space:]].*\.md$/ { cur = $0; next } /^[[:space:]]*[0-9]+:[0-9]+[[:space:]]/ { if (cur == want) print } ' } # --- 1. Each Vale hook resolves its config and gates its own file shape --- # Asserted per hook, against that hook's own fixture path and its own token. An # aggregate alert count over both hooks' combined output does not prove this: # one fixture description carries every flagged token, so ONE working hook # already clears a `>= 2` threshold. And a hook whose .vale.ini globs match # nothing reaches neither of the guards below — it still MATCHES the file via # its `files:` regex, so pre-commit does not report `Skipped`; vale simply lints # nothing, prints `0 errors ... in 1 file` and exits 0, and the hook shows # `Passed`. Attribution is the only thing that catches it. echo "" echo "--- each Vale hook flags its own fixture in an external consumer repo ---" write_fixtures "that helps with things" "that will utilize things" while IFS='|' read -r HOOK_ID FIXTURE TOKEN; do [[ -n "$HOOK_ID" ]] || continue LOG="$WORK/$HOOK_ID.log" set +e (cd "$CONSUMER" && pre-commit run "$HOOK_ID" --all-files > "$LOG" 2>&1) RC_HOOK=$? set -e if grep -q "does not exist" "$LOG"; then fail "$HOOK_ID hard-errored on a path resolved against the consumer repo (E100) — the bug this test guards against" sed 's/^/ /' "$LOG" elif grep -q "Skipped" "$LOG"; then fail "$HOOK_ID matched no files, so it proved nothing" sed 's/^/ /' "$LOG" elif [[ $RC_HOOK -eq 0 ]]; then fail "$HOOK_ID passed $FIXTURE despite its flagged '$TOKEN' — a .vale.ini glob matching nothing lints zero files and exits 0" sed 's/^/ /' "$LOG" elif alerts_for "$FIXTURE" < "$LOG" | grep -qF "'$TOKEN'"; then pass "$HOOK_ID flattens $FIXTURE and flags its '$TOKEN' in a consumer repo" else fail "$HOOK_ID failed, but no alert quoting '$TOKEN' was filed under $FIXTURE" sed 's/^/ /' "$LOG" fi done <<'EOF' kyberforge-vale-audit-skill|skills/demo/SKILL.md|helps with kyberforge-vale-audit-agent|agents/demo.md|utilize EOF # --- 2. Clean files pass — the hooks gate, they don't just always fail --- echo "" echo "--- all three hooks pass clean files in an external consumer repo ---" write_fixtures "of the packaged hook contract" set +e (cd "$CONSUMER" && pre-commit run --all-files > "$WORK/clean.log" 2>&1) RC_CLEAN=$? set -e if grep -q "Skipped" "$WORK/clean.log"; then fail "a hook matched no files on the clean run, so it proved nothing" sed 's/^/ /' "$WORK/clean.log" elif [[ $RC_CLEAN -eq 0 ]]; then pass "all three hooks exit 0 on clean files" else fail "hooks failed on clean files (rc=$RC_CLEAN)" sed 's/^/ /' "$WORK/clean.log" fi # --- 3. The size hook gates too. It ran clean above, which is what proves it # is executable and its entry path resolves; this half proves it still fails a # file that breaks the ceiling rather than passing everything. --- echo "" echo "--- kyberforge-skill-size-check fails an oversized SKILL.md in an external consumer repo ---" mkdir -p "$CONSUMER/skills/oversized" { echo "---" echo "name: oversized" echo "description: Use when the caller wants an oversized fixture." echo "---" for ((i = 1; i <= 600; i++)); do echo "word" done } > "$CONSUMER/skills/oversized/SKILL.md" git -C "$CONSUMER" add -A set +e (cd "$CONSUMER" && pre-commit run kyberforge-skill-size-check --all-files > "$WORK/size.log" 2>&1) RC_SIZE=$? set -e if [[ $RC_SIZE -ne 0 ]] && grep -q "500-line ceiling" "$WORK/size.log"; then pass "kyberforge-skill-size-check exits non-zero and names the ceiling it broke" else fail "kyberforge-skill-size-check did not gate an oversized SKILL.md (rc=$RC_SIZE)" sed 's/^/ /' "$WORK/size.log" fi echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]