aboutsummaryrefslogtreecommitdiff
path: root/languages/bash/tests/validate-bash.bats
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 21:13:26 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 21:13:26 -0400
commit36262858461711bcb104896007a513691113fee8 (patch)
tree333cc7ea0998c43c2b6af76aa6eeb1ee3d0b0f2c /languages/bash/tests/validate-bash.bats
parent71db71b9d47ffbeaf1d1c859fa3e3bebb7b2ea29 (diff)
downloadrulesets-36262858461711bcb104896007a513691113fee8.tar.gz
rulesets-36262858461711bcb104896007a513691113fee8.zip
feat(languages): add bash/shell bundle
Shell-heavy projects had no bundle that fit. archangel and archsetup are bash repos, and installing elisp or python gave them the wrong language rules. I added languages/bash on the go bundle's shape. The bundle ships bash.md and bash-testing.md rules, a PostToolUse hook that runs shellcheck on edited shell files and blocks on a violation, a shellcheck pre-commit githook, settings.json wiring, gitignore-add.txt, and a "Bash/shell project" CLAUDE.md. The hook covers .sh, .bash, and extensionless files with a shell shebang, since the CLI tools that fill a shell repo carry no extension. shellcheck is the gate. shfmt stays out of the blocking path because shell has no canonical formatting style, and forcing tabs-vs-spaces would impose a contested choice. Both the hook and the githook are shellcheck-clean against their own rule. I extended the Makefile test target to discover languages/*/tests/*.bats, so the bundle's 8 hook tests run with the rest of the suite. The README bundle table was stale, listing elisp only. I corrected it to the five bundles now shipping.
Diffstat (limited to 'languages/bash/tests/validate-bash.bats')
-rw-r--r--languages/bash/tests/validate-bash.bats96
1 files changed, 96 insertions, 0 deletions
diff --git a/languages/bash/tests/validate-bash.bats b/languages/bash/tests/validate-bash.bats
new file mode 100644
index 0000000..9f268a1
--- /dev/null
+++ b/languages/bash/tests/validate-bash.bats
@@ -0,0 +1,96 @@
+#!/usr/bin/env bats
+#
+# Tests for languages/bash/claude/hooks/validate-bash.sh — the PostToolUse hook
+# that runs shellcheck on edited shell files and blocks on a violation.
+#
+# The hook reads tool-call JSON on stdin and extracts the file path, so each
+# test pipes a JSON payload naming a real file it wrote into a temp dir. The
+# shellcheck dependency is real (integration): clean files pass, genuinely
+# broken ones fail. Tests needing shellcheck skip when it's absent so the suite
+# stays portable.
+
+HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/claude/hooks/validate-bash.sh"
+
+setup() {
+ TEST_DIR="$(mktemp -d -t validate-bash-bats.XXXXXX)"
+}
+
+teardown() {
+ rm -rf "$TEST_DIR"
+}
+
+# Build a tool-call JSON payload naming a file_path.
+payload() {
+ printf '{"tool_input": {"file_path": "%s"}}' "$1"
+}
+
+# ---- Normal ----------------------------------------------------------
+
+@test "validate-bash: a clean .sh file passes silently (exit 0)" {
+ command -v shellcheck >/dev/null 2>&1 || skip "shellcheck not installed"
+ printf '#!/usr/bin/env bash\nset -euo pipefail\necho "ok"\n' > "$TEST_DIR/clean.sh"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.sh")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+# ---- Error -----------------------------------------------------------
+
+@test "validate-bash: a shellcheck violation blocks (exit 2, names shellcheck)" {
+ command -v shellcheck >/dev/null 2>&1 || skip "shellcheck not installed"
+ # SC2086: unquoted expansion that word-splits — a real shellcheck warning.
+ printf '#!/usr/bin/env bash\nf=$1\nrm $f\n' > "$TEST_DIR/bad.sh"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.sh")"
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"SHELLCHECK"* ]]
+}
+
+# ---- Boundary --------------------------------------------------------
+
+@test "validate-bash: a non-shell file is ignored (exit 0)" {
+ printf 'print("hello")\n' > "$TEST_DIR/script.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/script.py")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "validate-bash: an extensionless file with a bash shebang is validated" {
+ command -v shellcheck >/dev/null 2>&1 || skip "shellcheck not installed"
+ printf '#!/usr/bin/env bash\nf=$1\nrm $f\n' > "$TEST_DIR/cli-tool"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/cli-tool")"
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"SHELLCHECK"* ]]
+}
+
+@test "validate-bash: an extensionless non-shell file is ignored (exit 0)" {
+ printf 'just some text\nno shebang here\n' > "$TEST_DIR/notes"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/notes")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "validate-bash: empty file_path is a no-op (exit 0)" {
+ run bash "$HOOK" <<< '{"tool_input": {}}'
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "validate-bash: a missing file is a no-op (exit 0)" {
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/does-not-exist.sh")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "validate-bash: shellcheck absent does not block the edit (exit 0)" {
+ # PATH with jq + coreutils symlinked but no shellcheck → hook can't validate,
+ # must not block.
+ STUB="$TEST_DIR/bin"
+ mkdir -p "$STUB"
+ for b in bash jq head cat printf grep sed; do
+ src="$(command -v "$b" 2>/dev/null)" && ln -sf "$src" "$STUB/$b"
+ done
+ printf '#!/usr/bin/env bash\nf=$1\nrm $f\n' > "$TEST_DIR/bad.sh"
+ run env PATH="$STUB" bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.sh")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}