#!/usr/bin/env bash # Regression test for scripts/vale-wrap.sh: Vale's `text.frontmatter.description` # NLP scope silently stops matching when the description value is a YAML block # scalar spanning 2+ physical lines. vale-wrap.sh flattens it to one line before # handing off to the real vale binary — this asserts that actually happens. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # skill-audit's copy is used here (not agent-audit's) because every fixture below is a # SKILL.md — only skill-audit's .vale.ini has the [**/SKILL.md] glob section. vale-wrap.sh # itself is an identical copy in both skills, so which one SCRIPT points at doesn't matter. SKILL_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/skill-audit" SCRIPT="$SKILL_AUDIT/scripts/vale-wrap.sh" VALE_CONFIG="$SKILL_AUDIT/assets/vale/.vale.ini" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } if ! command -v vale &>/dev/null; then echo "SKIP: vale is not installed — skipping (matches skill-audit/agent-audit's own fallback behavior)" exit 77 fi make_fixture() { local dir desc_lines file dir="$(mktemp -d)" (cd "$dir" && git init -q) mkdir -p "$dir/plugins/testplugin/skills/zzzskill" desc_lines="$1" file="$dir/plugins/testplugin/skills/zzzskill/SKILL.md" { echo "---" echo "name: zzzskill" echo "description: >" for ((i = 1; i <= desc_lines; i++)); do echo " Line $i mentions helps with and utilize, plus a colon: like this." done echo "---" echo "" echo "Body." } > "$file" echo "$dir" } # Every Kyberforge rule is `level: error`, so vale exits non-zero whenever a # fixture trips one — which is the expected outcome for nearly every case here. # run_wrap therefore captures output and swallows the exit status; assertions # are made on the report text. Cases that genuinely care about the exit code # capture it explicitly instead. run_wrap() { local dir="$1" shift (cd "$dir" && bash "$SCRIPT" "$@" 2>&1) || true } # --- 1. A known-bad single-line description is caught (sanity check on Vale itself) --- echo "" echo "--- catches vague wording in a single-line description ---" FIXTURE1="$(make_fixture 1)" trap 'rm -rf "$FIXTURE1"' EXIT if run_wrap "$FIXTURE1" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then pass "flags vague wording when description is a single physical line" else fail "did not flag known-bad single-line description" fi # --- 2. The same known-bad wording across 2+ physical lines is still caught --- echo "" echo "--- catches vague wording in a multi-line folded description ---" FIXTURE2="$(make_fixture 2)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT if run_wrap "$FIXTURE2" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then pass "flags vague wording when description spans 2+ physical lines" else fail "silently missed known-bad wording in a multi-line description — the bug this test guards against" fi # --- 3. Line count is preserved so unrelated report line numbers don't shift --- echo "" echo "--- preserves total line count when flattening ---" FIXTURE3="$(make_fixture 3)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3"' EXIT ORIG_LINES=$(wc -l < "$FIXTURE3/plugins/testplugin/skills/zzzskill/SKILL.md") OUT=$(run_wrap "$FIXTURE3" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md) MAX_LINE=$(echo "$OUT" | grep -oE '^[[:space:]]*[0-9]+:[0-9]+' | tr -d '[:space:]' | cut -d: -f1 | sort -n | tail -1) if [[ -n "$MAX_LINE" ]] && (( MAX_LINE <= ORIG_LINES )); then pass "reported line numbers stay within the original file's line count" else fail "reported line number ($MAX_LINE) exceeds original file line count ($ORIG_LINES)" fi # make_raw_fixture writes stdin verbatim to a fresh fixture's SKILL.md, for # cases where the exact description body needs to be hand-crafted rather than # generated from the desc_lines loop above. make_raw_fixture() { local dir dir="$(mktemp -d)" (cd "$dir" && git init -q) mkdir -p "$dir/plugins/testplugin/skills/zzzskill" cat > "$dir/plugins/testplugin/skills/zzzskill/SKILL.md" echo "$dir" } # --- 4. A folded description containing a double quote is still caught --- # This is the exact case that silently passed (zero alerts) before switching # from json.dumps (double-quoted, backslash-escaped) to a single-quoted scalar. echo "" echo "--- catches vague wording when the folded description contains a double quote ---" FIXTURE4="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Use when the user says "audit this skill" and helps with and utilize things. Second line continues the same folded scalar for flattening. --- Body. EOF )" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT if run_wrap "$FIXTURE4" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then pass "flags vague wording when the description contains a double quote" else fail "silently missed vague wording in a description containing a double quote — the bug this test guards against" fi # --- 5. A folded description containing an apostrophe fires and stays valid YAML --- echo "" echo "--- catches vague wording when the folded description contains an apostrophe ---" FIXTURE5="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Use when the user's task helps with and utilize things across two lines. Second continuation line for the fold. --- Body. EOF )" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT OUT5=$(run_wrap "$FIXTURE5" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md) if echo "$OUT5" | grep -q "VagueWording"; then pass "flags vague wording when the description contains an apostrophe" else fail "silently missed vague wording in a description containing an apostrophe" fi if echo "$OUT5" | grep -qi "yaml:"; then fail "flattened copy with an apostrophe produced a YAML parse error" else pass "flattened copy with an apostrophe is valid YAML (no parse error)" fi # --- 6. A folded description with a backslash and a non-ASCII character --- echo "" echo "--- catches vague wording when the folded description has a backslash and non-ASCII text ---" FIXTURE6="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Use when the café résumé naïve thing helps with and utilize things here. Path is C:\Users\test and this is the second continuation line. --- Body. EOF )" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT if run_wrap "$FIXTURE6" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then pass "flags vague wording when the description has a backslash and non-ASCII text" else fail "silently missed vague wording in a description with a backslash and non-ASCII text" fi # --- 7. A folded description with a blank line between two paragraphs --- echo "" echo "--- handles a blank line inside a folded description without crashing ---" FIXTURE7="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Use when the user needs a general helper. Do not use when this helps with and utilize things instead. --- Body. EOF )" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT OUT7=$(run_wrap "$FIXTURE7" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md) if echo "$OUT7" | grep -q "Traceback"; then fail "crashed while flattening a description with a blank line between paragraphs" elif echo "$OUT7" | grep -q "VagueWording"; then pass "still flags vague wording in the second paragraph after a blank line" else fail "silently missed vague wording in the second paragraph after a blank line — the bug this test guards against" fi # --- 8. Relative paths resolve against the caller's cwd, exactly as bare vale # resolves them. Every path below is deliberately relative to $SUBDIR8, not to # the fixture's repo root: an earlier version of the wrapper resolved relative # paths against the git toplevel instead, which (a) hard-errored on a # `--config ../../..` that bare vale accepts and (b) silently dropped file # arguments that didn't resolve from the repo root, skipping the flattening the # wrapper exists to perform. The old tests only ever passed repo-root-relative # paths from a subdirectory, so neither failure mode was caught. echo "" echo "--- resolves a cwd-relative --config from a subdirectory (equals and two-argv forms) ---" FIXTURE8="$(mktemp -d)" (cd "$FIXTURE8" && git init -q) cp "$VALE_CONFIG" "$FIXTURE8/.vale.ini" cp -r "$SKILL_AUDIT/assets/vale/styles" "$FIXTURE8/styles" mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill" { echo "---" echo "name: zzzskill" echo "description: >" echo " Line one mentions helps with and utilize, plus a colon: like this." echo " Line two continues the same folded scalar for flattening." echo "---" echo "" echo "Body." } > "$FIXTURE8/plugins/testplugin/skills/zzzskill/SKILL.md" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8"' EXIT SUBDIR8="$FIXTURE8/plugins/testplugin/skills/zzzskill" # Both paths are relative to $SUBDIR8 (four levels below the fixture root). REL_CFG8="../../../../.vale.ini" REL_FILE8="SKILL.md" OUT_EQ=$(run_wrap "$SUBDIR8" "--config=$REL_CFG8" "$REL_FILE8") OUT_TWO=$(run_wrap "$SUBDIR8" --config "$REL_CFG8" "$REL_FILE8") if echo "$OUT_EQ" | grep -q "VagueWording" && [[ "$OUT_EQ" == "$OUT_TWO" ]]; then pass "cwd-relative --config resolves from a subdirectory in both argv forms" else fail "cwd-relative --config did not resolve from a subdirectory (equals form vs two-argv form)" fi # --- 8b. A cwd-relative --config matches the equivalent absolute invocation --- # Regression for failure mode (a): resolving --config against the repo root made # `--config ../../../../.vale.ini` expand to a path above the toplevel, and vale # hard-errored with "does not exist" (exit 2) on args bare vale handles fine. echo "" echo "--- a cwd-relative --config produces the same result as the absolute-path form ---" set +e OUT_REL_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$REL_CFG8" "$REL_FILE8" 2>&1) RC_REL_CFG=$? OUT_ABS_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1) RC_ABS_CFG=$? set -e if echo "$OUT_REL_CFG" | grep -qi "does not exist"; then fail "cwd-relative --config hard-errored ('does not exist') — the bug this test guards against" elif [[ "$OUT_REL_CFG" == "$OUT_ABS_CFG" && "$RC_REL_CFG" -eq "$RC_ABS_CFG" ]]; then pass "cwd-relative --config matches the absolute-path invocation (output and exit code)" else fail "cwd-relative --config (rc=$RC_REL_CFG) diverged from the absolute-path form (rc=$RC_ABS_CFG)" fi # --- 8c. A cwd-relative FILE argument is still flattened, not silently skipped --- # Regression for failure mode (b): a relative file path that didn't resolve from # the repo root failed the wrapper's file test, fell through to the vale flag # list, and left the file list empty — so the wrapper exec'd bare vale and # silently skipped the flattening. Bare vale reports nothing here, so asserting # on the alert (not just the exit code) is what makes the silence detectable. echo "" echo "--- flattens a cwd-relative file argument passed from a subdirectory ---" WRAPPED_REL=$(run_wrap "$SUBDIR8" --config "$FIXTURE8/.vale.ini" "$REL_FILE8") BARE_REL=$(cd "$SUBDIR8" && vale --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1 || true) if ! echo "$WRAPPED_REL" | grep -q "VagueWording"; then fail "cwd-relative file argument produced no alert — flattening was silently skipped, the bug this test guards against" elif echo "$BARE_REL" | grep -q "VagueWording"; then fail "bare vale already flags this fixture, so the test can't detect a silently-skipped flattening" else pass "cwd-relative file argument is flattened and flagged where bare vale reports nothing" fi # --- 9. Zero file args (or a file list that filters to nothing) exits promptly --- echo "" echo "--- exits promptly instead of hanging on stdin when no files are passed ---" if timeout 5 bash "$SCRIPT" --config "$VALE_CONFIG" < <(sleep 100) >/dev/null 2>&1; then pass "exits promptly with zero file args" else RC=$? if [[ $RC -eq 124 ]]; then fail "hung waiting on stdin with zero file args — the bug this test guards against" else pass "exits promptly (nonzero exit) with zero file args" fi fi echo "" echo "--- exits promptly when a file list filters down to nothing ---" if timeout 5 bash "$SCRIPT" --config "$VALE_CONFIG" --no-such-flag < <(sleep 100) >/dev/null 2>&1; then pass "exits promptly when no file-shaped args remain" else RC=$? if [[ $RC -eq 124 ]]; then fail "hung waiting on stdin when the file list filtered to nothing — the bug this test guards against" else pass "exits promptly (nonzero exit) when the file list filters to nothing" fi fi # --- 10. An absolute path to the fixture SKILL.md is still linted, not skipped --- echo "" echo "--- lints an absolute path to a skill file instead of silently skipping it ---" FIXTURE10="$(make_fixture 2)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10"' EXIT ABS_FILE10="$FIXTURE10/plugins/testplugin/skills/zzzskill/SKILL.md" if run_wrap "$FIXTURE10" --config "$VALE_CONFIG" "$ABS_FILE10" | grep -q "VagueWording"; then pass "an absolute path is linted, not silently skipped" else fail "an absolute path was silently skipped — the bug this test guards against" fi # --- 11. A literal (|) block scalar passes through unflattened. Unlike every # other multi-line form, `|` is not broken in Vale: its parsed value keeps the # same line breaks the source has, so the description scope still matches. The # second assertion pins that down — without it, a wrapper that broke `|` and a # Vale that never matched `|` would agree on zero alerts and the comparison # would pass vacuously. echo "" echo "--- leaves a literal (|) block scalar untouched (narrowed >-only scope) ---" FIXTURE11="$(make_raw_fixture <<'EOF' --- name: zzzskill description: | Line one mentions helps with and utilize things here. Line two continues the literal block scalar for this test. --- Body. EOF )" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT REL11="plugins/testplugin/skills/zzzskill/SKILL.md" WRAPPED_OUT=$(run_wrap "$FIXTURE11" --config "$VALE_CONFIG" "$REL11") BARE_OUT=$(cd "$FIXTURE11" && vale --config "$VALE_CONFIG" "$REL11" 2>&1 || true) if ! echo "$BARE_OUT" | grep -q "VagueWording"; then fail "bare vale reports nothing for a literal (|) block scalar — the 'literal blocks are not broken' premise is wrong" elif [[ "$WRAPPED_OUT" == "$BARE_OUT" ]]; then pass "literal (|) block scalar output matches bare vale exactly — untouched by flattening" else fail "wrapper altered output for a literal (|) block scalar description — should be left untouched" fi # --- 12. With no --config at all, the wrapper falls back to its own sibling # assets/vale/.vale.ini. `.pre-commit-hooks.yaml` relies on this: pre-commit # prefixes only entry[0] with the hook-repo clone path, so a --config argument # there resolves against the consuming repo and hard-errors (E100) for every # external consumer. echo "" echo "--- defaults --config to the wrapper's own sibling assets/vale/.vale.ini ---" FIXTURE12="$(make_fixture 2)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12"' EXIT OUT12=$(run_wrap "$FIXTURE12" plugins/testplugin/skills/zzzskill/SKILL.md) if echo "$OUT12" | grep -q "VagueWording"; then pass "a --config-less invocation uses the wrapper's bundled config" else fail "a --config-less invocation found no config — external pre-commit consumers get E100, the bug this test guards against" fi # --- 13. No GNU-only `realpath -m`. macOS ships the BSD realpath, which has no # -m (canonicalize-missing) — and every scratch destination is a path that does # not exist yet, so a plain `realpath` exits 1 and set -e aborts the hook. echo "" echo "--- runs with a BSD realpath that has no -m option ---" STUB13="$(mktemp -d)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13"' EXIT REAL_REALPATH="$(command -v realpath || echo /bin/false)" cat > "$STUB13/realpath" <&2 exit 1 ;; esac done exec "$REAL_REALPATH" "\$@" EOF chmod +x "$STUB13/realpath" OUT13=$(cd "$FIXTURE12" && PATH="$STUB13:$PATH" bash "$SCRIPT" --config "$VALE_CONFIG" \ plugins/testplugin/skills/zzzskill/SKILL.md 2>&1 || true) if echo "$OUT13" | grep -q "illegal option"; then fail "invoked realpath -m — fails on macOS's BSD realpath, the bug this test guards against" elif echo "$OUT13" | grep -q "VagueWording"; then pass "flattens and flags with no GNU realpath available" else fail "produced no alert under a BSD-style realpath: $OUT13" fi # --- 14. A directory argument is walked and its files flattened. The classifier # used to accept only regular files, so a directory fell through to the vale # flag list, left the file list empty, and exec'd bare vale — silently skipping # the flattening. `lint`'s vale-run skill documents `vale ` as # normal usage, so this is a reachable path. echo "" echo "--- flattens files reached through a directory argument ---" FIXTURE14="$(make_fixture 2)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14"' EXIT WRAPPED_DIR=$(run_wrap "$FIXTURE14" --config "$VALE_CONFIG" plugins) BARE_DIR=$(cd "$FIXTURE14" && vale --config "$VALE_CONFIG" plugins 2>&1 || true) if ! echo "$WRAPPED_DIR" | grep -q "VagueWording"; then fail "a directory argument produced no alert — flattening was silently skipped, the bug this test guards against" elif echo "$BARE_DIR" | grep -q "VagueWording"; then fail "bare vale already flags this fixture, so the test can't detect a silently-skipped flattening" else pass "a directory argument is walked and its files flattened" fi # --- 15. Directory walking must survive paths with spaces --- echo "" echo "--- walks a directory containing a path with spaces ---" SPACED15="$FIXTURE14/plugins/testplugin/skills/zzz skill" mkdir -p "$SPACED15" cp "$FIXTURE14/plugins/testplugin/skills/zzzskill/SKILL.md" "$SPACED15/SKILL.md" rm -rf "$FIXTURE14/plugins/testplugin/skills/zzzskill" OUT15=$(run_wrap "$FIXTURE14" --config "$VALE_CONFIG" plugins/testplugin/skills) if echo "$OUT15" | grep -q "zzz skill" && echo "$OUT15" | grep -q "VagueWording"; then pass "a file under a directory whose name contains a space is walked and flattened" else fail "a path with a space was dropped from the directory walk" fi # --- 16. No unguarded `"${arr[@]}"` expansion survives in any script that runs # on macOS. bash before 4.4 — including the 3.2 that macOS still ships as # /bin/bash — treats that form on an *empty* array as an unbound variable under # `set -u` and aborts. The portable form is `${arr[@]+"${arr[@]}"}`. This is a # static check because no bash 5 host can reproduce the abort at runtime: the # construct is only fatal on the older shell, so absence of the construct is the # property to assert. `${#arr[@]}` is deliberately not flagged — the count form # is safe on 3.2. Neither is an array seeded with at least one element where it # is declared and never reset to empty: it cannot be empty at any expansion # site, so the construct is not a hazard there and demanding the guarded form # would be a wrong test. The file list covers every script this repo ships or # runs that a macOS user reaches: the wrapper itself, the two pre-commit hook # scripts, and the test runner AGENTS.md tells contributors to run by hand. # `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan # cannot see it — run-tests.sh carried one until it was replaced with a # `while read` loop, and nothing would have caught its return. `declare -A` # (bash 4.0+ associative arrays) is checked for the same reason — the # expansion scan cannot see it, and check-vale-style-sync.sh carried a pair of # them until they were replaced with index-scanned plain arrays. echo "" echo "--- no unguarded array expansion remains in the macOS-facing scripts ---" unguarded_expansions() { local file="$1" hit name while IFS= read -r hit; do name="$(printf '%s\n' "$hit" \ | grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' | head -1 \ | sed -E 's/^\$\{//; s/\[@\]\}$//')" if grep -qE "^[[:space:]]*((local|declare|readonly)[[:space:]]+)?(-a[[:space:]]+)?$name=\([^)]" "$file" \ && ! grep -qE "^[[:space:]]*$name=\(\)" "$file"; then continue fi printf '%s:%s\n' "${file##*/}" "$hit" done < <( # Blank out whole-line comments (keeping line numbers), delete every # correctly guarded expansion, then anything still matching is a candidate. awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$file" \ | sed -E 's/\$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\+"\$\{\1\[@\]\}"\}//g' \ | grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' || true ) } HAZARDS16="" for BASH32_SCRIPT in \ "$SCRIPT" \ "$REPO_ROOT/scripts/skill-size-check.sh" \ "$REPO_ROOT/scripts/check-release-needed.sh" \ "$REPO_ROOT/scripts/check-vale-style-sync.sh" \ "$REPO_ROOT/tests/run-tests.sh"; do FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")" if [[ -n "$FOUND16" ]]; then HAZARDS16+="$FOUND16 " fi # `mapfile`/`readarray` are bash 4.0+ builtins with no 3.2 fallback. Whole-line # comments are blanked first so prose naming the builtin is not a hit. FOUND16B="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \ | grep -nE '(^|[^[:alnum:]_])(mapfile|readarray)[[:space:]]' || true)" if [[ -n "$FOUND16B" ]]; then HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B " fi # `declare -A` (associative arrays) is bash 4.0+ with no 3.2 fallback. The # flag cluster can carry other letters in any order (-Ag, -rA, ...); what # matters is a literal uppercase A appearing in it, so match on that rather # than the exact string "-A". FOUND16C="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \ | grep -nE '(^|[^[:alnum:]_])declare[[:space:]]+-[a-zA-Z]*A[a-zA-Z]*([[:space:]]|$)' || true)" if [[ -n "$FOUND16C" ]]; then HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16C " fi done if [[ -n "$HAZARDS16" ]]; then fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')" else pass "every array expansion uses the bash-3.2-safe \${arr[@]+\"\${arr[@]}\"} form" fi # --- 17. The invocations whose arrays are closest to empty actually run. Under # a bash older than 4.4 this is genuine macOS-shell coverage; on a modern bash it # degrades to a smoke test, so the pass message names the shell that really ran. # Point VALE_WRAP_TEST_BASH at a 3.2 build to get the real thing in CI. echo "" echo "--- degenerate invocations survive on the oldest available bash ---" OLD_BASH="bash" OLD_BASH_VER="$(bash -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')" for CAND in "${VALE_WRAP_TEST_BASH:-}" bash-3.2 bash3 /bin/bash /usr/local/bin/bash; do [[ -n "$CAND" ]] && command -v "$CAND" >/dev/null 2>&1 || continue CAND_VER="$("$CAND" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"' 2>/dev/null)" || continue [[ -n "$CAND_VER" ]] || continue if (( ${CAND_VER%.*} * 100 + ${CAND_VER#*.} < ${OLD_BASH_VER%.*} * 100 + ${OLD_BASH_VER#*.} )); then OLD_BASH="$CAND" OLD_BASH_VER="$CAND_VER" fi done FIXTURE17="$(make_fixture 2)" mkdir -p "$FIXTURE17/emptydir" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14" "$FIXTURE17"' EXIT # Zero args, flags with no path, and a directory that walks to nothing are the # three shapes that leave vale_args/path_args/argv_paths at their emptiest. OUT17="" for ARGS17 in "" "--config $VALE_CONFIG" "--config $VALE_CONFIG emptydir"; do # shellcheck disable=SC2086 # deliberate word splitting of the argv fixture OUT17+="$( (cd "$FIXTURE17" && "$OLD_BASH" "$SCRIPT" $ARGS17 &1) || true)" done if echo "$OUT17" | grep -q "unbound variable"; then fail "aborted with 'unbound variable' on bash $OLD_BASH_VER — the bug this test guards against" else pass "degenerate invocations run clean under bash $OLD_BASH_VER ($OLD_BASH)" fi # --- 18. The guarded expansion must keep argv word boundaries intact. Dropping # the quotes (`${arr[@]}`) also silences the unbound-variable abort, so it is the # tempting wrong fix — and it splits any path containing a space into two bogus # arguments. Case 15 covers spaces found by the directory walk; this covers a # space in the path argument itself, which is what argv_paths expands. echo "" echo "--- a path argument containing a space survives the guarded expansion ---" FIXTURE18="$(make_fixture 2)" trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14" "$FIXTURE17" "$FIXTURE18"' EXIT SPACED18="$FIXTURE18/plugins/testplugin/skills/zzz skill dir" mkdir -p "$SPACED18" mv "$FIXTURE18/plugins/testplugin/skills/zzzskill/SKILL.md" "$SPACED18/SKILL.md" OUT18=$(run_wrap "$FIXTURE18" --config "$VALE_CONFIG" "plugins/testplugin/skills/zzz skill dir/SKILL.md") if echo "$OUT18" | grep -q "zzz skill dir/SKILL.md" && echo "$OUT18" | grep -q "VagueWording"; then pass "a path argument with a space is passed to vale as one word" else fail "a path argument with a space was split by the array expansion: $OUT18" fi # The cases below share one cleanup list. The per-case trap rebuilding above # does not scale past the fixture count it already carries, and this trap is # installed last, so it is the one that runs. EXTRA_FIXTURES=() new_fixture() { EXTRA_FIXTURES+=("$1"); } cleanup_all() { rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" \ "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" \ "$FIXTURE14" "$FIXTURE17" "$FIXTURE18" \ ${EXTRA_FIXTURES[@]+"${EXTRA_FIXTURES[@]}"} } trap cleanup_all EXIT # --- 19. Every YAML form whose parsed value is joined back out of 2+ physical # lines breaks the `text.frontmatter.description` scope identically, not just # the `>` folded block the flattener originally handled: a plain scalar wrapped # onto continuation lines, a double-quoted one, a single-quoted one, and a bare # `description:` whose value starts on the next line all report zero alerts # under bare vale. Each must come back with the same alerts as the single-line # spelling of the same sentence. Line and column numbers legitimately move (the # value lands on one physical line), so the comparison drops the `line:col` # prefix and compares the alert text — message, matched token, and rule name. REL_SKILL19="plugins/testplugin/skills/zzzskill/SKILL.md" DESC19_A="Use when the caller helps with a specific job" DESC19_B="and the second physical line will utilize the wrap" # make_form_fixture spells the same two-clause description in one YAML scalar # form: single, folded, plain, dquote, squote, or keyonly. make_form_fixture() { local form="$1" dir dir="$(mktemp -d)" new_fixture "$dir" (cd "$dir" && git init -q) mkdir -p "$dir/plugins/testplugin/skills/zzzskill" { echo "---" echo "name: zzzskill" case "$form" in single) echo "description: $DESC19_A $DESC19_B" ;; folded) echo "description: >"; echo " $DESC19_A"; echo " $DESC19_B" ;; plain) echo "description: $DESC19_A"; echo " $DESC19_B" ;; dquote) echo "description: \"$DESC19_A"; echo " $DESC19_B\"" ;; squote) echo "description: '$DESC19_A"; echo " $DESC19_B'" ;; keyonly) echo "description:"; echo " $DESC19_A"; echo " $DESC19_B" ;; *) echo "make_form_fixture: unknown form '$form'" >&2; exit 1 ;; esac echo "---" echo "" echo "Body." } > "$dir/plugins/testplugin/skills/zzzskill/SKILL.md" echo "$dir" } # Alert text with the `line:col` prefix and ANSI colouring stripped, sorted. # The `|| true` matters under this file's `set -o pipefail`: a report with no # alerts at all makes grep exit 1, which would abort the whole run inside the # command substitutions below — silently, before the empty-baseline guard could # print anything. Returning empty output instead is what makes that guard # reachable. alert_text() { echo "$1" \ | sed -E 's/\x1b\[[0-9;]*m//g' \ | { grep -oE '(error|warning|suggestion)[[:space:]]+.*' || true; } \ | sed -E 's/[[:space:]]+/ /g' \ | sort } echo "" echo "--- every multi-line description form reports what its single-line form reports ---" FIXTURE19_SINGLE="$(make_form_fixture single)" BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")" if [[ -z "$BASELINE19" ]]; then # The loop below has to be skipped, not merely reported on: an empty baseline # compares equal to five empty results, so it would print five vacuous PASSes # alongside this one FAIL. The FAIL alone still fails the run at the end. fail "the single-line baseline reported nothing — the comparisons below would be vacuous, so they are skipped" else for FORM19 in folded plain dquote squote keyonly; do DIR19="$(make_form_fixture "$FORM19")" BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)" GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")" if echo "$BARE19" | grep -q "VagueWording"; then fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening" elif [[ "$GOT19" == "$BASELINE19" ]]; then pass "a $FORM19 multi-line description reports the same alerts as its single-line form" else fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]" fi done fi # --- 20. A style token containing an ASCII apostrophe matches inside a # flattened description. The flattener used to substitute U+2019 for every `'` # before writing the scratch copy, so no rule whose token carried an apostrophe # could ever fire on a flattened description — a silent, rule-shaped blind spot. # All three branches that can hold an apostrophe are exercised: a value that is # safe unquoted; one that must be quoted (it contains `: `) and so lands in a # double-quoted scalar, since a single-quoted one would need the `''` escape # that kills the scope outright; and one that also holds a double quote, which # no inline scalar can spell verbatim and which therefore lands in a `|-` # literal block. echo "" echo "--- a style token containing an apostrophe matches in a flattened description ---" APOS_STYLE="$(mktemp -d)" new_fixture "$APOS_STYLE" mkdir -p "$APOS_STYLE/styles/Apostrophe" cat > "$APOS_STYLE/styles/Apostrophe/Token.yml" <<'EOF' extends: existence message: "apostrophe token: '%s'" level: error scope: text.frontmatter.description ignorecase: true tokens: - "user's task" EOF # A body-scoped companion rule, used by case 20b to read back the line number of # a line *after* the frontmatter — the only way to catch the blank-line pad # being off in either direction. cat > "$APOS_STYLE/styles/Apostrophe/Body.yml" <<'EOF' extends: existence message: "body token: '%s'" level: error scope: text tokens: - flattening marker phrase EOF cat > "$APOS_STYLE/.vale.ini" <<'EOF' StylesPath = styles [**/SKILL.md] BasedOnStyles = Apostrophe EOF FIXTURE20_PLAIN="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Use when the user's task needs handling, and a second physical line continues the folded scalar. --- Body. EOF )" new_fixture "$FIXTURE20_PLAIN" FIXTURE20_QUOTED="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Triggers on: the user's task needing handling, and a second physical line continues the folded scalar. --- Body. EOF )" new_fixture "$FIXTURE20_QUOTED" # Needs quoting (`: `), holds an apostrophe AND a double quote — the one # combination no inline scalar can carry, so this is the `|-` literal-block # branch. The VagueWording tokens are there for case 20b, which reuses it. FIXTURE20_BLOCK="$(make_raw_fixture <<'EOF' --- name: zzzskill description: > Triggers on: the user's task and "audit this" phrasing, which helps with and utilize things across a second physical line. --- Body carrying a flattening marker phrase for the line-number check. EOF )" new_fixture "$FIXTURE20_BLOCK" for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED" \ "literal-block:$FIXTURE20_BLOCK"; do if run_wrap "${CASE20#*:}" --config "$APOS_STYLE/.vale.ini" "$REL_SKILL19" \ | grep -q "Apostrophe.Token"; then pass "an apostrophe-bearing token matches in a flattened ${CASE20%%:*} description" else fail "an apostrophe-bearing token was rewritten out of a flattened ${CASE20%%:*} description" fi done # --- 20b. The `|-` literal-block branch that case 20 just proved lossless must # also keep the rest of the scope working and keep the line accounting right. # The block is 2 physical lines where every inline form is 1, so the blank-line # pad that preserves later line numbers has to drop by one. The second # assertion pins that arithmetic against the body line's true number: case 3's # `<= original line count` bound would not, since a pad that is one line short # shifts every later line *up*, staying inside the bound while still lying. echo "" echo "--- the |- literal-block fallback lints normally and preserves line numbers ---" OUT20B=$(run_wrap "$FIXTURE20_BLOCK" --config "$VALE_CONFIG" "$REL_SKILL19") if echo "$OUT20B" | grep -q "VagueWording"; then pass "a description needing quotes with both an apostrophe and a double quote is still linted" else fail "a description needing quotes with both an apostrophe and a double quote produced no alerts" fi WANT20B_LINE="$(grep -n 'flattening marker phrase' "$FIXTURE20_BLOCK/$REL_SKILL19" | cut -d: -f1)" # `--output line` prints `file:line:col:Rule:message`, so the line number reads # back without any wrapping or colour to strip. GOT20B_LINE="$(run_wrap "$FIXTURE20_BLOCK" --config "$APOS_STYLE/.vale.ini" --output line "$REL_SKILL19" \ | grep 'Apostrophe.Body' | head -1 | cut -d: -f2)" if [[ "$GOT20B_LINE" == "$WANT20B_LINE" ]]; then pass "a body line after a |- flattened description keeps its original line number ($WANT20B_LINE)" else fail "the |- block's blank-line pad shifted the body: vale reported line $GOT20B_LINE, the file has it at $WANT20B_LINE" fi # --- 21. A symlinked file inside a directory argument is mirrored and linted. # Vale follows symlinks (both a symlinked file and a file under a symlinked # directory), so a `-type f` walk of the tree reported "0 files" where bare vale # reports one — and the audit skills read a "0 files" report as NOT RUN. echo "" echo "--- mirrors a symlinked file reached through a directory argument ---" FIXTURE21="$(make_fixture 2)" new_fixture "$FIXTURE21" mkdir -p "$FIXTURE21/real" mv "$FIXTURE21/$REL_SKILL19" "$FIXTURE21/real/SKILL.md" ln -s ../../../../real/SKILL.md "$FIXTURE21/$REL_SKILL19" BARE21="$(cd "$FIXTURE21" && vale --config "$VALE_CONFIG" plugins 2>&1 || true)" WRAPPED21="$(run_wrap "$FIXTURE21" --config "$VALE_CONFIG" plugins)" BARE21_FILES="$(echo "$BARE21" | sed -E 's/\x1b\[[0-9;]*m//g' | grep -oE 'in [0-9]+ files?' | tail -1)" WRAPPED21_FILES="$(echo "$WRAPPED21" | sed -E 's/\x1b\[[0-9;]*m//g' | grep -oE 'in [0-9]+ files?' | tail -1)" if [[ "$BARE21_FILES" != "in 1 file" ]]; then fail "bare vale did not lint the symlinked file ($BARE21_FILES), so this case can't detect the walk dropping it" elif [[ "$WRAPPED21_FILES" != "$BARE21_FILES" ]]; then fail "the directory walk dropped a symlinked file: wrapper saw '$WRAPPED21_FILES', bare vale '$BARE21_FILES'" elif echo "$WRAPPED21" | grep -q "VagueWording"; then pass "a symlinked file under a directory argument is mirrored, flattened and flagged" else fail "a symlinked file was mirrored but not flattened — no alert came back" fi # --- 22. The value of a separated two-argv flag is never treated as a lint # target, however file-like it looks. `--output tmpl.tmpl` names a real # template file: classifying it as input both linted the template and reordered # argv, so vale received `--output --no-wrap` and died on `open :`. echo "" echo "--- a separated flag value that names a real file is not linted as a target ---" FIXTURE22="$(make_fixture 1)" new_fixture "$FIXTURE22" printf 'TMPL{{range .Files}} {{.Path}}{{end}}\n' > "$FIXTURE22/tmpl.tmpl" WRAPPED22="$(run_wrap "$FIXTURE22" --config "$VALE_CONFIG" --output tmpl.tmpl --no-wrap "$REL_SKILL19")" BARE22="$(cd "$FIXTURE22" && vale --config "$VALE_CONFIG" --output tmpl.tmpl --no-wrap "$REL_SKILL19" 2>&1 || true)" # The fixture's description is a single physical line, so flattening is a no-op # and the two invocations must agree byte for byte. if [[ "$WRAPPED22" == "$BARE22" ]]; then pass "a separated --output value is passed through to vale, not linted" else fail "a separated --output value was misrouted: wrapper gave [$WRAPPED22], bare vale [$BARE22]" fi # --- 23. A path argument that does not exist is a hard error. Bare vale drops # it, falls back to stdin and prints `0 errors ... in stdin` with exit 0, so a # typo'd target is indistinguishable from a clean run — and the audit skills' # NOT RUN guard string-matches `0 files`, which `in stdin` never produces. This # is a deliberate divergence from bare vale, documented in the wrapper header. echo "" echo "--- a nonexistent path argument fails loudly instead of falling back to stdin ---" set +e OUT23="$(cd "$FIXTURE22" && bash "$SCRIPT" --config "$VALE_CONFIG" plugins/testplugin/skills/zzzskill/SKILLL.md 2>&1)" RC23=$? set -e if [[ $RC23 -eq 0 ]]; then fail "a typo'd path exited 0 — indistinguishable from a clean run, the bug this test guards against" elif echo "$OUT23" | grep -q "in stdin"; then fail "a typo'd path fell back to reading stdin and reported 'in stdin' instead of erroring" elif echo "$OUT23" | grep -q "SKILLL.md"; then pass "a typo'd path exits nonzero with a message naming the path" else fail "a typo'd path exited $RC23 but the message does not name it: $OUT23" fi # --- 24. `--output`'s built-in style names must not be path-absolutized. The # wrapper rewrites path-valued flag values to absolute form so they still # resolve after the `cd` into the scratch mirror, deciding with an `-e` # existence test — but `line`, `JSON` and `CLI` are style names, not paths. With # a file or directory of that name sitting in the caller's cwd the test hit, the # built-in became `$cwd/line`, and vale flipped into template mode and died with # `E100 [template] Runtime error` where bare vale prints a normal report. echo "" echo "--- a built-in --output style name survives a same-named entry in the cwd ---" FIXTURE24="$(make_fixture 2)" new_fixture "$FIXTURE24" mkdir -p "$FIXTURE24/line" : > "$FIXTURE24/JSON" for FORM24 in "--output line" "--output=line" "--output JSON" "--output=JSON"; do # shellcheck disable=SC2086 # deliberate word splitting of the argv fixture OUT24="$(run_wrap "$FIXTURE24" --config "$VALE_CONFIG" $FORM24 "$REL_SKILL19")" if echo "$OUT24" | grep -q "E100"; then fail "'$FORM24' was rewritten to a cwd path and vale flipped into template mode — the bug this test guards against" elif echo "$OUT24" | grep -q "VagueWording"; then pass "'$FORM24' is passed through as a built-in style name" else fail "'$FORM24' produced no alert: $OUT24" fi done echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]