Files
holocron/tests/test-vale-wrap.sh
Defame1297 413a750819 fix(scripts): close gates that passed while the thing they guard was disabled
Four repo gates reported success in states they exist to reject.

`check-vale-style-sync.sh` passed while a Kyberforge lint rule was silenced. The
check matched a blocklist of severity values, but Vale's semantic is an allowlist:
anything that is not exactly YES/error/warning/suggestion disables the rule. So
`= false`, `= 0`, `= garbage`, an empty value and — worst — a lowercase `= yes` all
killed enforcement while reading as "enabled" to a human. Inverted to an allowlist.
Two sibling holes: dropping `KyberforgeCopilot` from `BasedOnStyles` unloaded the
Copilot-only check silently, and narrowing a section glob to a location made Vale
lint zero files, which is the "0 files, hook Passed" failure the script's own
comment says it exists to catch.

`sync-marketplace-mirror.sh --check` failed open when its source was missing, while
its sibling correctly errored in the same state.

`check-scope-walkup-sync.sh` wrote to hardcoded `/tmp/fN.out` paths and read one
back, making it non-reentrant — a concurrent instance can flip a verdict, and this
branch made the test runner concurrent. Now per-run `mktemp -d`.

`check-manifests.sh` had no disk-to-marketplace pass, so a plugin directory absent
from `marketplace.json` passed every gate while the `validate-plugins` hook globbed
it. The "listed" match is restricted to remote-source entry names; matching any
entry name let a genuine orphan through on a name coincidence.

`run-bats.sh` reported an empty TAP stream as `0 tests, 0 failures`, exit 0 — a
total harness failure reading as a pass.

The test-side changes are the larger half, because the guards were the real problem.
`test-sync-marketplace-mirror.sh` could overwrite the live tracked mirror under an
inherited GIT_DIR, which is precisely the git-hook context it runs in. The bash-3.2
scan hand-maintained its file list, omitting the new shared runner, and had no rule
for `wait -n` or `nproc` — the two hazards the previous review round found live. It
now derives 43 files across three globs with per-glob floors. Several assertions
were decoration: the concurrency checks caught the reentrancy defect 0 times in 10,
the leak fix was green either way, and two manifest fixtures passed with the code
they claimed to cover deleted. Every assertion now has a revert it provably fails
against.

Refs: #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 01:52:56 +00:00

1097 lines
52 KiB
Bash
Executable File

#!/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/.apm/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" <<EOF
#!/usr/bin/env bash
for a in "\$@"; do
case "\$a" in
-m|--canonicalize-missing)
echo "realpath: illegal option -- m" >&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 <path-or-glob>` 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. Neither is an expansion whose own line first proves the
# array non-empty (`[[ ${#a[@]} -eq 0 ]] || rm -rf "${a[@]}"`) — the short-circuit
# means the expansion is unreachable when the array is empty.
#
# The scanned file list is DERIVED, not hand-maintained. A hardcoded list only
# guards the scripts someone remembered to add to it, and it silently fails to
# cover anything new: it omitted scripts/lib/batch-run.sh — the shared runner
# this branch introduced, whose own header (batch-run.sh:9-11) documents it as
# bash-3.2-safe — along with four other scripts/*.sh. Deriving the list means a
# new script is covered the moment it lands. AGENTS.md names bash 3.2 as an
# explicit repo target, so the scope is three globs, each floor-asserted below:
# - scripts/**/*.sh — repo tooling and pre-commit hook scripts
# - tests/*.sh — the runners and every regression test
# - plugins/*/.apm/**/*.sh — the scripts plugins ship to users
# plugins/*/skills/** is deliberately NOT scanned: it is the generated mirror of
# .apm/, so scanning both double-reports every finding, and mirror-vs-source
# drift is already sync-plugin-content.sh --check's job. Scanning .apm/ is what
# closed the gap where skill-audit's vale-wrap.sh was covered but agent-audit's
# byte-identical copy of it was not.
# providers/**/*.sh is the one shipped script deliberately left out:
# providers/claude-code/statusline-command.sh seeds `parts=()` empty at :83 and
# expands it unguarded at :96. That is a real latent hazard rather than a false
# positive — it just cannot abort today because the file enables no `set -u`.
# Fixing it is a change to a file this case does not own; once :96 uses the
# guarded form, add a `providers` glob to the table below.
#
# Four constructs are checked, because the expansion scan cannot see any of the
# other three:
# - `mapfile`/`readarray` — bash 4.0+ builtins. run-tests.sh carried one until
# it was replaced with a `while read` loop.
# - `declare -A` — bash 4.0+ associative arrays. check-vale-style-sync.sh
# carried a pair until they became index-scanned plain arrays.
# - `wait -n` — bash 4.3+. A prior review round found this live in run-bats.sh.
# - `nproc` — GNU coreutils, absent on macOS entirely. Same review round, same
# file. `getconf _NPROCESSORS_ONLN` is the portable spelling batch-run.sh
# settled on.
# The last two had no static guard anywhere before this, and
# `shellcheck --severity=warning` (.pre-commit-config.yaml) pins no target
# version, so it does not catch them either — meaning the guard against the last
# regression would not have caught the last regression.
echo ""
echo "--- no bash-4-only construct remains in the macOS-facing scripts ---"
# Files a script pulls in via `source`, as named by its `# shellcheck source=`
# directives. Array seeding often lives in the sourced file (install.sh's
# DEPLOY_* come from deploy-manifest.sh), so the seeding check has to look
# there too or it reports a false hazard. shellcheck resolves these paths
# against either the repo root or the script's own directory depending on
# configuration, so both are tried and whichever exists is used.
sourced_files() {
local file="$1" rel cand
while IFS= read -r rel; do
for cand in "$REPO_ROOT/$rel" "$(dirname "$file")/$rel"; do
if [[ -f "$cand" ]]; then
printf '%s\n' "$cand"
break
fi
done
done < <(
grep -oE '^[[:space:]]*#[[:space:]]*shellcheck[[:space:]]+source=[^[:space:]]+' "$file" \
| sed -E 's/.*source=//' || true
)
return 0
}
unguarded_expansions() {
local file="$1" hit name seed_file
local seed_files
seed_files=("$file")
while IFS= read -r seed_file; do
seed_files+=("$seed_file")
done < <(sourced_files "$file")
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/\[@\]\}$//')"
# Shell-maintained arrays are never seeded by a `NAME=(...)` line, so the
# seeding exemption below can never clear them: without this case they are
# permanent false positives. Exempted are the ones measured non-empty inside
# a running script -- element counts taken at a script's top level:
# PIPESTATUS >=1 once any command has run (0 only before the very first,
# where the variable is meaningless anyway)
# BASH_SOURCE 1 (one frame per sourced/executed file)
# BASH_LINENO 1 (maintained in parallel with BASH_SOURCE)
# BASH_VERSINFO 6 (always exactly six)
# GROUPS 1
# DIRSTACK 1 (always holds at least the current directory)
#
# FUNCNAME, BASH_ARGV, BASH_ARGC, BASH_REMATCH and COMP_WORDS are
# deliberately NOT exempted despite being shell-maintained, because they are
# genuinely empty in reachable states: FUNCNAME is 0 outside a function,
# BASH_ARGV is 0 without `shopt -s extdebug`, BASH_ARGC is 0 *inside a
# function* (it looks safe when measured at top level, where it is 1 -- it is
# not), BASH_REMATCH is 0 until a `=~` match succeeds, COMP_WORDS is 0
# outside completion. Expanding any of those bare really does abort on bash
# 3.2 under `set -u`, so flagging them is the correct answer rather than a
# false positive. Case 26 pins this split so neither half drifts.
case "$name" in
PIPESTATUS|BASH_SOURCE|BASH_LINENO|BASH_VERSINFO|GROUPS|DIRSTACK) continue ;;
esac
# Same-line emptiness short-circuit: the expansion cannot be reached empty.
if printf '%s\n' "$hit" \
| grep -qE "\\\$\{#$name\[@\]\}[[:space:]]*-(eq|lt)[[:space:]]*[01][^|]*\|\|"; then
continue
fi
for seed_file in ${seed_files[@]+"${seed_files[@]}"}; do
# `([^)]|$)` after the paren, not just `[^)]`: a multi-line declaration
# (`DEPLOY_FILES=(` with its elements on the following lines) ends the line
# right there, and requiring a character after the paren missed it. An
# empty `name=()` still does not match, which is what the check is for.
if grep -qE "^[[:space:]]*((local|declare|readonly)[[:space:]]+)?(-a[[:space:]]+)?$name=\(([^)]|$)" "$seed_file" \
&& ! grep -qE "^[[:space:]]*$name=\(\)" "$seed_file"; then
continue 2
fi
done
printf '%s:%s\n' "${file##*/}" "$hit"
done < <(
# Blank out whole-line comments (keeping line numbers), delete every
# correctly guarded expansion — in both its bare spelling and the
# backslash-escaped one that appears inside this file's own PASS message —
# 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
)
}
# Blanks WHOLE-LINE comments only (keeping line numbers so hits stay locatable),
# so prose naming a hazard on its own line is not a hit.
#
# KNOW THIS BEFORE YOU EDIT ANY SCANNED FILE. Nothing else is stripped — this is
# a line-based scanner, not a shell parser — so all of the following DO trip the
# case even though none is a real hazard:
# true # avoid nproc on macOS <- trailing comment naming a hazard
# echo "avoid mapfile in scripts" <- hazard named inside a string
# <<EOF ... nproc ... EOF <- hazard named in a heredoc body
# foo # see ${x[@]} <- trailing comment holding an expansion
# Teaching it to parse shell would cost far more than it returns, so the rule is:
# put the mention on its own comment line, or break the token with a one-character
# bracket class the way the `npro[c]` rule below does — `npro[c]` matches exactly
# what a bare spelling would while containing no bare spelling itself. This
# matters because a static check that cries wolf is a static check someone
# eventually deletes.
strip_comments() { awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$1"; }
bash32_glob() {
case "$1" in
scripts) find "$REPO_ROOT/scripts" -name '*.sh' -type f ;;
tests) find "$REPO_ROOT/tests" -maxdepth 1 -name '*.sh' -type f ;;
plugins) find "$REPO_ROOT/plugins" -path '*/.apm/*' -name '*.sh' -type f ;;
*) echo "bash32_glob: unknown glob '$1'" >&2; return 1 ;;
esac
}
# Floors are PER GLOB, not on the merged total. A single total floor cannot
# detect the failure this assertion exists to name: with 12 + 17 + 13 files,
# losing the whole `scripts` glob still leaves 30 and losing the whole `tests`
# glob still leaves 25, so any total floor low enough to survive normal churn
# is too low to notice an entire glob silently resolving to nothing. Each floor
# sits a little under its current count so ordinary file removal does not trip
# it, but a broken or renamed path does. Parallel arrays rather than an
# associative one — `declare -A` is bash 4.0+, which this very case forbids.
BASH32_GLOB_NAMES=(scripts tests plugins)
BASH32_GLOB_FLOORS=(10 14 10)
BASH32_SCRIPTS=()
BASH32_IDX=0
while [[ $BASH32_IDX -lt ${#BASH32_GLOB_NAMES[@]} ]]; do
BASH32_GLOB="${BASH32_GLOB_NAMES[$BASH32_IDX]}"
BASH32_FLOOR="${BASH32_GLOB_FLOORS[$BASH32_IDX]}"
BASH32_COUNT=0
while IFS= read -r BASH32_FOUND; do
BASH32_SCRIPTS+=("$BASH32_FOUND")
BASH32_COUNT=$((BASH32_COUNT + 1))
done < <(bash32_glob "$BASH32_GLOB" | sort)
if [[ $BASH32_COUNT -lt $BASH32_FLOOR ]]; then
fail "the bash-3.2 scan's '$BASH32_GLOB' glob derived $BASH32_COUNT file(s), under its floor of $BASH32_FLOOR — that path is wrong, so those scripts are silently unscanned"
fi
BASH32_IDX=$((BASH32_IDX + 1))
done
HAZARDS16=""
for BASH32_SCRIPT in ${BASH32_SCRIPTS[@]+"${BASH32_SCRIPTS[@]}"}; do
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
if [[ -n "$FOUND16" ]]; then
HAZARDS16+="$FOUND16 "
fi
# `mapfile`/`readarray`: bash 4.0+ builtins with no 3.2 fallback.
# `declare -A`: bash 4.0+ associative arrays. 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".
# `wait -n`: bash 4.3+. `nproc`: GNU coreutils, not present on macOS.
# The last two close on `[^[:alnum:]_]`, not `[[:space:]]`: the real spellings
# are `$(nproc)` and `wait -n;`, and a whitespace-only terminator misses both.
# `npro[c]` matches exactly what `nproc` would, but keeps the literal string
# "nproc" out of this file — the scan reads this file too, so a bare spelling
# here would report itself as a hazard.
for BASH32_RULE in \
'(^|[^[:alnum:]_])(mapfile|readarray)[[:space:]]' \
'(^|[^[:alnum:]_])declare[[:space:]]+-[a-zA-Z]*A[a-zA-Z]*([[:space:]]|$)' \
'(^|[^[:alnum:]_])wait[[:space:]]+-n([^[:alnum:]_]|$)' \
'(^|[^[:alnum:]_])npro[c]([^[:alnum:]_]|$)'; do
FOUND16B="$(strip_comments "$BASH32_SCRIPT" | grep -nE "$BASH32_RULE" || true)"
if [[ -n "$FOUND16B" ]]; then
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
fi
done
done
if [[ -n "$HAZARDS16" ]]; then
fail "bash-4-only construct(s) found in ${#BASH32_SCRIPTS[@]} scanned script(s): $(echo "$HAZARDS16" | tr '\n' ' ')"
else
# This message deliberately names none of the four hazards in their literal
# spelling: the scan reads this file too, so a literal here is indistinguishable
# from a real occurrence and the case would fail on its own success message.
pass "all ${#BASH32_SCRIPTS[@]} scanned scripts are free of every bash-4-only construct this case checks for"
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 </dev/null 2>&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. It prints the fixture
# dir and does NOT call new_fixture itself: every caller invokes it inside `$( )`,
# so a registration made in here would land in the command substitution's
# subshell and never reach cleanup_all -- which leaked all six fixtures per run.
# Registration is therefore the caller's job, in the caller's shell.
make_form_fixture() {
local form="$1" dir
dir="$(mktemp -d)"
(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 ---"
# FORM_FIXTURES19 is bookkeeping for case 25, which asserts every dir recorded
# here also reached EXTRA_FIXTURES. It is appended to independently of
# new_fixture so that dropping the new_fixture calls is detectable.
FORM_FIXTURES19=()
FIXTURE19_SINGLE="$(make_form_fixture single)"
FORM_FIXTURES19+=("$FIXTURE19_SINGLE")
new_fixture "$FIXTURE19_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")"
FORM_FIXTURES19+=("$DIR19")
new_fixture "$DIR19"
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
# --- 25. Every fixture this run created is on the cleanup list. make_form_fixture
# used to call `new_fixture` itself, but every caller invokes it as
# `$(make_form_fixture ...)` — a command substitution — so the append landed in a
# subshell and was gone by the time the caller resumed. cleanup_all then removed
# five of the six form fixtures it never heard about, leaking one temp dir per
# YAML form per run (six in total, measured under a private TMPDIR).
#
# Nothing in the suite noticed: a leak fails no assertion, and the run reported
# "39 passed, 0 failed" with the bug present exactly as it does with the bug
# fixed. Leak-freeness was only ever observable by watching TMPDIR from outside,
# which is not a regression test. This case makes it one — it compares the dirs
# case 19 created against the dirs registered for cleanup, so re-introducing the
# subshell registration fails the run rather than quietly leaking again.
echo ""
echo "--- every fixture created by this run is registered for cleanup ---"
UNREGISTERED25=""
for DIR25 in ${FORM_FIXTURES19[@]+"${FORM_FIXTURES19[@]}"}; do
REGISTERED25=0
for KNOWN25 in ${EXTRA_FIXTURES[@]+"${EXTRA_FIXTURES[@]}"}; do
if [[ "$KNOWN25" == "$DIR25" ]]; then
REGISTERED25=1
break
fi
done
if [[ $REGISTERED25 -eq 0 ]]; then
UNREGISTERED25+="$DIR25 "
fi
done
if [[ ${#FORM_FIXTURES19[@]} -ne 6 ]]; then
fail "expected 6 form fixtures to have been created, saw ${#FORM_FIXTURES19[@]} — case 25 is not checking what it claims"
elif [[ -n "$UNREGISTERED25" ]]; then
fail "fixture(s) created but never registered for cleanup, so they leak: $UNREGISTERED25"
else
pass "all ${#FORM_FIXTURES19[@]} form fixtures are registered for cleanup"
fi
# --- 26. Case 16's shell-special-array exemption is pinned to a fixture. No file
# the scan currently reads expands any of those arrays, so the exemption is inert
# in practice: it could be deleted, or quietly widened to cover an array that can
# genuinely be empty, and every existing case would still pass. This drives the
# real unguarded_expansions over a fixture holding one expansion of each, and
# asserts the split in BOTH directions -- exempt arrays absent from the findings,
# never-safe arrays present. The membership is not cosmetic: the exempt ones are
# shell-maintained and always non-empty, while the flagged ones are empty in
# reachable states (see the comment on the exemption for the measured counts), so
# widening the list to include one of the latter would suppress a real abort.
echo ""
echo "--- the shell-special-array exemption covers exactly the never-empty arrays ---"
FIXTURE26="$(mktemp -d)"
new_fixture "$FIXTURE26"
EXEMPT26="PIPESTATUS BASH_SOURCE BASH_LINENO BASH_VERSINFO GROUPS DIRSTACK"
FLAGGED26="FUNCNAME BASH_ARGV BASH_ARGC BASH_REMATCH COMP_WORDS"
{
echo "#!/usr/bin/env bash"
for ARR26 in $EXEMPT26 $FLAGGED26; do
echo "echo \"\${${ARR26}[@]}\""
done
} > "$FIXTURE26/probe.sh"
FOUND26="$(unguarded_expansions "$FIXTURE26/probe.sh")"
MISSING26=""
LEAKED26=""
for ARR26 in $EXEMPT26; do
if echo "$FOUND26" | grep -q "{$ARR26\[@\]}"; then
LEAKED26+="$ARR26 "
fi
done
for ARR26 in $FLAGGED26; do
if ! echo "$FOUND26" | grep -q "{$ARR26\[@\]}"; then
MISSING26+="$ARR26 "
fi
done
if [[ -n "$LEAKED26" ]]; then
fail "always-non-empty shell array(s) reported as hazards, so the exemption is not applying: $LEAKED26"
elif [[ -n "$MISSING26" ]]; then
fail "shell array(s) that CAN be empty were exempted, suppressing a real bash-3.2 abort: $MISSING26"
else
pass "all 6 never-empty shell arrays are exempt and all 5 sometimes-empty ones are still flagged"
fi
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]