Vale locates a frontmatter description by matching the parsed YAML value back against the source text, so any scalar whose value is not spelled out verbatim loses the `text.frontmatter.description` scope entirely. The wrapper only flattened `>` folded scalars, so plain, double-quoted and single-quoted multi-line descriptions silently reported zero alerts and exit 0 — a clean pass indistinguishable from a real one, in a prefilter whose callers are instructed not to re-derive its verdict by judgment. Implementation notes: - Classify the scalar kind after `^description:[ \t]*` and reuse one shared continuation-line generator for every form; `|` literal blocks keep their line breaks, stay verbatim-matchable, and are still left untouched. - Emit the flattened value in whichever scalar form needs no escape at all (plain, then single-quoted, then double-quoted), because any escape breaks the verbatim match. The old blanket `'` -> U+2019 substitution silently made apostrophe-bearing rule tokens unmatchable across 63% of the corpus; it now survives only for the one combination no YAML scalar can carry verbatim. - Terminate continuations at a line flush with the key, not only on a shallower indent — a `description:` followed by a flush-left line previously swallowed the rest of the frontmatter. - Route vale's value-taking flags explicitly instead of inferring targets by file existence, and absolutize relative `--output`/`--path` values the way `--config` already was, since the run `cd`s into the scratch mirror. - Fail loudly on a nonexistent path instead of inheriting bare vale's fallback to stdin, which rendered a typo'd path as `0 errors ... in stdin`, exit 0 — a form the callers' `0 files` NOT-RUN guard cannot match. - Follow symlinks when walking a directory argument, matching bare vale. Refs: #85
772 lines
34 KiB
Bash
Executable File
772 lines
34 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. 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. The file list covers every script this repo ships or
|
|
# runs that a macOS user reaches: the wrapper itself plus the two pre-commit
|
|
# hook scripts.
|
|
echo ""
|
|
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
|
|
unguarded_expansions() {
|
|
local file="$1" hit name
|
|
while IFS= read -r hit; do
|
|
name="$(printf '%s\n' "$hit" \
|
|
| grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' | head -1 \
|
|
| sed -E 's/^\$\{//; s/\[@\]\}$//')"
|
|
if grep -qE "^[[:space:]]*((local|declare|readonly)[[:space:]]+)?(-a[[:space:]]+)?$name=\([^)]" "$file" \
|
|
&& ! grep -qE "^[[:space:]]*$name=\(\)" "$file"; then
|
|
continue
|
|
fi
|
|
printf '%s:%s\n' "${file##*/}" "$hit"
|
|
done < <(
|
|
# Blank out whole-line comments (keeping line numbers), delete every
|
|
# correctly guarded expansion, then anything still matching is a candidate.
|
|
awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$file" \
|
|
| sed -E 's/\$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\+"\$\{\1\[@\]\}"\}//g' \
|
|
| grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' || true
|
|
)
|
|
}
|
|
HAZARDS16=""
|
|
for BASH32_SCRIPT in \
|
|
"$SCRIPT" \
|
|
"$REPO_ROOT/scripts/skill-size-check.sh" \
|
|
"$REPO_ROOT/scripts/check-release-needed.sh"; do
|
|
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
|
|
if [[ -n "$FOUND16" ]]; then
|
|
HAZARDS16+="$FOUND16 "
|
|
fi
|
|
done
|
|
if [[ -n "$HAZARDS16" ]]; then
|
|
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
|
else
|
|
pass "every array expansion uses the bash-3.2-safe \${arr[@]+\"\${arr[@]}\"} form"
|
|
fi
|
|
|
|
# --- 17. The invocations whose arrays are closest to empty actually run. Under
|
|
# a bash older than 4.4 this is genuine macOS-shell coverage; on a modern bash it
|
|
# degrades to a smoke test, so the pass message names the shell that really ran.
|
|
# Point VALE_WRAP_TEST_BASH at a 3.2 build to get the real thing in CI.
|
|
echo ""
|
|
echo "--- degenerate invocations survive on the oldest available bash ---"
|
|
OLD_BASH="bash"
|
|
OLD_BASH_VER="$(bash -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')"
|
|
for CAND in "${VALE_WRAP_TEST_BASH:-}" bash-3.2 bash3 /bin/bash /usr/local/bin/bash; do
|
|
[[ -n "$CAND" ]] && command -v "$CAND" >/dev/null 2>&1 || continue
|
|
CAND_VER="$("$CAND" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"' 2>/dev/null)" || continue
|
|
[[ -n "$CAND_VER" ]] || continue
|
|
if (( ${CAND_VER%.*} * 100 + ${CAND_VER#*.} < ${OLD_BASH_VER%.*} * 100 + ${OLD_BASH_VER#*.} )); then
|
|
OLD_BASH="$CAND"
|
|
OLD_BASH_VER="$CAND_VER"
|
|
fi
|
|
done
|
|
FIXTURE17="$(make_fixture 2)"
|
|
mkdir -p "$FIXTURE17/emptydir"
|
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14" "$FIXTURE17"' EXIT
|
|
# Zero args, flags with no path, and a directory that walks to nothing are the
|
|
# three shapes that leave vale_args/path_args/argv_paths at their emptiest.
|
|
OUT17=""
|
|
for ARGS17 in "" "--config $VALE_CONFIG" "--config $VALE_CONFIG emptydir"; do
|
|
# shellcheck disable=SC2086 # deliberate word splitting of the argv fixture
|
|
OUT17+="$( (cd "$FIXTURE17" && "$OLD_BASH" "$SCRIPT" $ARGS17 </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.
|
|
make_form_fixture() {
|
|
local form="$1" dir
|
|
dir="$(mktemp -d)"
|
|
new_fixture "$dir"
|
|
(cd "$dir" && git init -q)
|
|
mkdir -p "$dir/plugins/testplugin/skills/zzzskill"
|
|
{
|
|
echo "---"
|
|
echo "name: zzzskill"
|
|
case "$form" in
|
|
single) echo "description: $DESC19_A $DESC19_B" ;;
|
|
folded) echo "description: >"; echo " $DESC19_A"; echo " $DESC19_B" ;;
|
|
plain) echo "description: $DESC19_A"; echo " $DESC19_B" ;;
|
|
dquote) echo "description: \"$DESC19_A"; echo " $DESC19_B\"" ;;
|
|
squote) echo "description: '$DESC19_A"; echo " $DESC19_B'" ;;
|
|
keyonly) echo "description:"; echo " $DESC19_A"; echo " $DESC19_B" ;;
|
|
*) echo "make_form_fixture: unknown form '$form'" >&2; exit 1 ;;
|
|
esac
|
|
echo "---"
|
|
echo ""
|
|
echo "Body."
|
|
} > "$dir/plugins/testplugin/skills/zzzskill/SKILL.md"
|
|
echo "$dir"
|
|
}
|
|
|
|
# Alert text with the `line:col` prefix and ANSI colouring stripped, sorted.
|
|
alert_text() {
|
|
echo "$1" \
|
|
| sed -E 's/\x1b\[[0-9;]*m//g' \
|
|
| grep -oE '(error|warning|suggestion)[[:space:]]+.*' \
|
|
| sed -E 's/[[:space:]]+/ /g' \
|
|
| sort
|
|
}
|
|
|
|
echo ""
|
|
echo "--- every multi-line description form reports what its single-line form reports ---"
|
|
FIXTURE19_SINGLE="$(make_form_fixture single)"
|
|
BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
|
if [[ -z "$BASELINE19" ]]; then
|
|
fail "the single-line baseline reported nothing — the comparison below would be vacuous"
|
|
fi
|
|
for FORM19 in folded plain dquote squote keyonly; do
|
|
DIR19="$(make_form_fixture "$FORM19")"
|
|
BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)"
|
|
GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")"
|
|
if echo "$BARE19" | grep -q "VagueWording"; then
|
|
fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening"
|
|
elif [[ "$GOT19" == "$BASELINE19" ]]; then
|
|
pass "a $FORM19 multi-line description reports the same alerts as its single-line form"
|
|
else
|
|
fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]"
|
|
fi
|
|
done
|
|
|
|
# --- 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.
|
|
# Both branches that can hold an apostrophe verbatim are exercised: a value that
|
|
# is safe unquoted, and one that must be quoted (it contains `: `) and so has to
|
|
# land in a double-quoted scalar, since a single-quoted one would need the `''`
|
|
# escape that kills the scope outright.
|
|
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
|
|
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"
|
|
for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED"; 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 one combination no verbatim YAML scalar can carry — needs
|
|
# quoting, holds an apostrophe, and holds a double quote — falls back to the
|
|
# lossy U+2019 substitution. Apostrophe-bearing tokens are lost there by
|
|
# design, but the scope must stay alive so every other rule still fires.
|
|
echo ""
|
|
echo "--- the unrepresentable combination keeps the description scope alive ---"
|
|
FIXTURE20C="$(make_raw_fixture <<'EOF'
|
|
---
|
|
name: zzzskill
|
|
description: >
|
|
Triggers on: the user's "audit this" phrasing, which helps with
|
|
and utilize things across a second physical line.
|
|
---
|
|
|
|
Body.
|
|
EOF
|
|
)"
|
|
new_fixture "$FIXTURE20C"
|
|
if run_wrap "$FIXTURE20C" --config "$VALE_CONFIG" "$REL_SKILL19" | 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
|
|
|
|
# --- 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
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|