Why: eight comments and one status note cited CONTEXT.md or AGENTS.md text thatb9c7762and1929ffdmoved or deleted. All are inert at runtime, but they are the rationale comments that tell the next maintainer why an assertion exists, and they now name a file that no longer explains it. Implementation notes: re-anchored by what the citation is for, not uniformly. - Four sites quoted facts ADR-0013 owns — every rule is `level: error` with no ignorable tier (ADR-0013:59-70), and KyberforgeCopilot's `.agent.md`-only scope (ADR-0013:43-46). These now cite ADR-0013. ADRs are append-only here; the spec docs are refactored, which is what caused this rot. - Two sites quoted the glob location-independence property, which no ADR owns. The quote was already inline and carried the full rationale, so the citation added a rot surface and no information — dropped, statement kept. - sync-marketplace-mirror.sh's header attributed the mirror-not-a-profile fact to CONTEXT.md; the parenthetical beside it already carries the evidence, so the attribution is dropped rather than re-pointed. - .pre-commit-config.yaml cited an AGENTS.md instruction that no longer exists; generalised to "the documented instruction". - LESSONS.md:29 misquoted AGENTS.md's current session-start line. Also corrects a pre-existing misattribution at tests/test-vale-wrap.sh:454: AGENTS.md has never named bash 3.2 as a repo target (`git log -S'3.2'` on it is empty). LESSONS.md and the script headers do. Impact: no behaviour change. test-check-vale-style-sync.sh and test-vale-wrap.sh both pass (42 passed, 0 failed).
1200 lines
58 KiB
Bash
Executable File
1200 lines
58 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. LESSONS.md and the script headers
|
|
# name bash 3.2 as an explicit repo target, so the scope is four 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
|
|
# - providers/**/*.sh — the scripts install.sh deploys to user machines
|
|
# 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 was excluded until issue #96: statusline-command.sh seeded
|
|
# `parts=()` empty and expanded it unguarded, a real latent hazard rather than a
|
|
# false positive, in a file this case did not own. That expansion is now guarded,
|
|
# so the glob is in the table below and the deployed provider scripts get the
|
|
# same coverage as scripts/, tests/ and plugins/*/.apm/. This is the glob most
|
|
# worth having: install.sh copies providers/ content onto every user machine, and
|
|
# it is also the glob most likely to look redundant to a future reader, because
|
|
# the hazard it catches is invisible on any modern bash — bash 4.4 stopped
|
|
# treating an empty-array expansion as unbound, so a bare "${a[@]}" under `set -u`
|
|
# runs clean on the maintainer's bash 5 and aborts only on macOS's bash 3.2.
|
|
# A runtime test cannot demonstrate that without a 3.2 binary; this static scan
|
|
# is the enforcement, which is why its floor must never be dropped to zero.
|
|
#
|
|
# 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 ;;
|
|
providers) find "$REPO_ROOT/providers" -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 + 18 + 13 + 1 files,
|
|
# losing the whole `scripts` glob still leaves 32 and losing the whole `tests`
|
|
# glob still leaves 26, 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.
|
|
#
|
|
# `providers` is floored at 1 rather than "current count minus slack" because it
|
|
# holds exactly one file: any slack at all would be a floor of 0, which passes on
|
|
# a renamed or deleted directory and is precisely the silent-zero failure the
|
|
# per-glob floors exist to catch. A floor of 1 means removing the last provider
|
|
# script trips this assertion — correct, because at that point the glob is dead
|
|
# weight and should be deleted from the table deliberately, not left to pass
|
|
# vacuously.
|
|
BASH32_GLOB_NAMES=(scripts tests plugins providers)
|
|
BASH32_GLOB_FLOORS=(10 14 10 1)
|
|
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
|
|
|
|
# Case 27 pins the `sourced_files()` exemption against the failure mode that
|
|
# actually happened (issue #97 item 2): a stale source-directive path.
|
|
#
|
|
# The exemption exists because array seeding often lives in the sourced file
|
|
# rather than the sourcing one -- install.sh's DEPLOY_* come from
|
|
# deploy-manifest.sh -- so without it those expansions read as hazards. It works
|
|
# by parsing each file's own source-directive comments. That makes it silently
|
|
# dependent on those paths resolving: a directive naming a file that is not
|
|
# there does not error, it just contributes no seed file, and the exemption
|
|
# quietly stops covering what it was written to cover.
|
|
#
|
|
# tests/run-tests.sh shipped exactly that for the length of PR #95 -- a directive
|
|
# resolving to neither the repo root nor the script's own directory. Nothing
|
|
# caught it: shellcheck's own SC1091 is `info`, and .pre-commit-config.yaml pins
|
|
# `--severity=warning`. The live consequence was nil only because both of
|
|
# run-tests.sh's expansions were independently guarded.
|
|
#
|
|
# Part C is the assertion that would have caught it, and it is the reason this
|
|
# case is not just fixture theatre: it holds every real directive in the scanned
|
|
# corpus to the resolution rule, so the next stale one fails here rather than
|
|
# lying dormant. Parts A and B pin the mechanism Part C depends on -- that
|
|
# resolution is what switches the exemption on, and non-resolution silently
|
|
# switches it off.
|
|
echo ""
|
|
echo "--- every shellcheck source directive resolves, so no seeding exemption is silently off ---"
|
|
FIXTURE27="$(mktemp -d)"
|
|
new_fixture "$FIXTURE27"
|
|
# The seeded array lives ONLY in the sourced file, never in the sourcing one --
|
|
# that separation is the whole point of the exemption.
|
|
{
|
|
echo "#!/usr/bin/env bash"
|
|
echo "SEEDED27=(a b c)"
|
|
} > "$FIXTURE27/seed-lib.sh"
|
|
|
|
# Both fixture bodies are emitted through printf with the closing `]` passed as
|
|
# an argument, never written literally. This file is itself inside the `tests`
|
|
# glob, so a literal bare expansion here would be scanned as a hazard in
|
|
# test-vale-wrap.sh's own source -- the trailing-comment/inside-a-string caveat
|
|
# documented above strip_comments(). Splitting the token means the emitted
|
|
# fixture holds the real text while this file holds no bare spelling, the same
|
|
# trick the `npro[c]` rule uses.
|
|
EXPANSION27="$(printf 'echo "${SEEDED27[@%s}"' ']')"
|
|
|
|
# Part A: a directive that resolves against the script's own directory exempts
|
|
# the expansion, so no hazard is reported.
|
|
{
|
|
echo "#!/usr/bin/env bash"
|
|
echo "# shellcheck source=seed-lib.sh"
|
|
echo 'source "$(dirname "$0")/seed-lib.sh"'
|
|
echo "$EXPANSION27"
|
|
} > "$FIXTURE27/resolves.sh"
|
|
|
|
# Part B: byte-identical except the directive names a file that is not there.
|
|
{
|
|
echo "#!/usr/bin/env bash"
|
|
echo "# shellcheck source=nonexistent/seed-lib.sh"
|
|
echo 'source "$(dirname "$0")/seed-lib.sh"'
|
|
echo "$EXPANSION27"
|
|
} > "$FIXTURE27/stale.sh"
|
|
|
|
RESOLVES27="$(unguarded_expansions "$FIXTURE27/resolves.sh")"
|
|
STALE27="$(unguarded_expansions "$FIXTURE27/stale.sh")"
|
|
|
|
# Part C: no real directive in the derived corpus may fail to resolve. Counted
|
|
# rather than diffed, because sourced_files() reports resolution only by
|
|
# omission -- an unresolvable directive produces no output line at all.
|
|
UNRESOLVED27=""
|
|
for BASH32_SCRIPT in ${BASH32_SCRIPTS[@]+"${BASH32_SCRIPTS[@]}"}; do
|
|
DECLARED27="$(grep -cE '^[[:space:]]*#[[:space:]]*shellcheck[[:space:]]+source=[^[:space:]]+' "$BASH32_SCRIPT" || true)"
|
|
[[ "$DECLARED27" -gt 0 ]] || continue
|
|
RESOLVED27="$(sourced_files "$BASH32_SCRIPT" | grep -c . || true)"
|
|
if [[ "$RESOLVED27" -lt "$DECLARED27" ]]; then
|
|
UNRESOLVED27+="${BASH32_SCRIPT#"$REPO_ROOT"/} ($RESOLVED27/$DECLARED27) "
|
|
fi
|
|
done
|
|
|
|
if [[ -n "$RESOLVES27" ]]; then
|
|
fail "an array seeded only in a resolvable sourced file was reported as a hazard, so the seeding exemption is not reading source directives at all"
|
|
elif [[ -z "$STALE27" ]]; then
|
|
fail "an array seeded only in an UNRESOLVABLE sourced file was still exempted, so this case cannot detect a stale directive and Part C proves nothing"
|
|
elif [[ -n "$UNRESOLVED27" ]]; then
|
|
fail "shellcheck source directive(s) resolve to nothing, silently disarming the seeding exemption for those files: $UNRESOLVED27"
|
|
else
|
|
pass "the seeding exemption switches on only for resolvable directives, and every directive in the scanned corpus resolves"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|