The external-consumer test asserted a combined alert count (>=2) across both shipped Vale hooks, but the SKILL.md fixture alone raises two alerts — so one working hook satisfied the threshold. Retargeting agent-audit's glob to match nothing left the suite reporting "3 passed" under the message "both hooks flatten and flag". The Skipped guard does not catch this: the hook still matches the file, Vale lints nothing, reports 0 errors in 1 file and exits 0, which pre-commit renders as Passed. An assertion aggregating over N subjects proves nothing about any individual subject. Each hook now runs individually and its alerts are attributed to the nearest preceding path header, so an alert is checked by path rather than by presence in the combined blob. The two fixtures carry distinct VagueWording tokens, so one hook's alert cannot be credited to another. Nothing in the repo read either .vale.ini — the sync check diffed only vale-wrap.sh and styles/Kyberforge, so a one-line glob typo silently disabled the prefilter for a whole file type. That was the enabling half of the same defect. The check now asserts the shared lines both copies must carry (StylesPath, a section naming Kyberforge as a whole word) without flagging their intentional divergence, and probes each glob section by asking Vale itself to lint a representative path. Regex-to-glob comparison was rejected as it means reimplementing doublestar semantics in bash; a file-count dry-run was rejected because a section whose glob matches but whose BasedOnStyles lost Kyberforge reports "1 file" with no alerts and would pass it. Every new assertion is bound to a failing case in both directions: breaking the artifact fails the suite, and neutering the assertion fails exactly one case. That reverse sweep exposed two assertions bound to no failing case at all, one masked by a stronger check running first. Refs: #85
196 lines
7.5 KiB
Bash
Executable File
196 lines
7.5 KiB
Bash
Executable File
#!/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" <<EOF
|
|
repos:
|
|
- repo: file://$HOOK_REPO
|
|
rev: $HOOK_REV
|
|
hooks:
|
|
- id: kyberforge-vale-audit-skill
|
|
- id: kyberforge-vale-audit-agent
|
|
- id: kyberforge-skill-size-check
|
|
EOF
|
|
|
|
# The two fixtures carry DIFFERENT flagged tokens so an alert can never be
|
|
# credited to the hook that did not raise it. Both bodies land mid-sentence in a
|
|
# folded block scalar that still spans two physical lines, which is the
|
|
# flattening the wrapper exists to do.
|
|
write_fixtures() {
|
|
local skill_body="$1"
|
|
local agent_body="${2:-$1}"
|
|
cat > "$CONSUMER/skills/demo/SKILL.md" <<EOF
|
|
---
|
|
name: demo
|
|
description: >
|
|
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" <<EOF
|
|
---
|
|
name: demo
|
|
description: >
|
|
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 ]]
|