Under set -u, "${arr[@]}" on an empty array aborts on bash before 4.4,
which is what macOS ships as /bin/bash. Three expansion sites now use
${arr[@]+"${arr[@]}"} consistently.
The hazard is not currently reachable: verified on a bash 3.2.57 built
from source that all seven invocation shapes succeed against the
previous code, including zero args, flags-only and an empty directory.
vale_args is provably non-empty at every site because the default
--config branch always appends first. The guard is kept because that
invariant is non-local and untested, so an edit to the default-config
branch would reintroduce a macOS-only crash silently.
Test fidelity is deliberately mixed. Case 16 is static and is the only
one that fails against the previous code, since no bash 5 host can
reproduce the abort at runtime. Case 17 runs the emptiest invocations
under the oldest bash it can find and names that shell in its output
so it cannot overclaim. Case 18 guards against the tempting wrong fix
of dropping the quotes, which also silences the abort but word-splits
a path containing a space.
No other bash 4.x construct is present; swept for mapfile, declare -A,
case modification, negative indices, globstar, wait -n and namerefs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
506 lines
23 KiB
Bash
Executable File
506 lines
23 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/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 (no regression) ---
|
|
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 [[ "$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 the wrapper. 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.
|
|
echo ""
|
|
echo "--- no unguarded array expansion remains in vale-wrap.sh ---"
|
|
unguarded_expansions() {
|
|
# Blank out whole-line comments (keeping line numbers), delete every correctly
|
|
# guarded expansion, then anything still matching is a real hazard.
|
|
awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$1" \
|
|
| sed -E 's/\$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\+"\$\{\1\[@\]\}"\}//g' \
|
|
| grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' || true
|
|
}
|
|
HAZARDS16="$(unguarded_expansions "$SCRIPT")"
|
|
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 </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
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|