Files
claude-foundations/formatters/tests/test-sh-formatter.sh
Paul O'Reilly dda01ebeca formatters/sh: SETE-2 must scan the whole file, not line by line
SETE-2 matched `sh -c "..."` with a per-line regex requiring both quotes on one
line. The shape that motivated the check spans lines:

    sh -c "
        hugo mod get ...
        hugo mod tidy
    "

so the real sync-deps bug — the one this check exists for — was never flagged.
Verified against the actual pre-fix sync-deps: now reports at line 358,
"4 commands and no 'set -e' inside".

Scans the file text with re.DOTALL and reports the line of the match start.
Falls back to counting non-blank lines when the body is newline- rather than
;-separated, which is what the multi-line form looks like.

Adds both regression cases (multi-line without set -e must flag; multi-line
with set -e must not). Verified they go red when the whole-file scan is
disabled. Neither of the two fixed real scripts is flagged.

Note for whoever extends this suite: fixtures written with a heredoc inside it
did not behave; the printf form does. Both new tests use printf.

Claude-Session: https://claude.ai/code/session_014KNyfHmHpMbdMrHZXDRkAp
2026-09-23 19:49:33 +12:00

268 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# tests/test-sh-formatter.sh — assert formatters/sh runs SETE-1/2 and LEAK-1
#
# House convention: bash, set -uo pipefail, SKIP-and-exit-0 when a tool is
# missing, one PASS:/FAIL: line per assertion, FAIL=1 aggregate, ALL_OK on
# full pass.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FORMATTER="$SCRIPT_DIR/../sh"
# --- SKIP gates: missing tool → exit 0 silently ---
if ! command -v python3 >/dev/null 2>&1; then
echo "SKIP: python3 not on PATH — formatters/sh custom checks need it"
exit 0
fi
if [ ! -x "$FORMATTER" ]; then
echo "SKIP: formatters/sh not found or not executable at $FORMATTER"
exit 0
fi
PASS=0
FAIL=0
pass() {
printf "PASS: %s\n" "$1"
PASS=$((PASS + 1))
}
fail() {
printf "FAIL: %s\n" "$1"
[[ -n "${2:-}" ]] && printf " %s\n" "$2"
FAIL=$((FAIL + 1))
}
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
# Write a fixture and run the formatter. Assert:
# - exit code matches expected_exit
# - stderr contains the pattern (when expect_pattern is set)
assert_flagged() {
local label="$1"
local expected_exit="$2"
local pattern="$3"
local content="$4"
local f="$TMPDIR/$(printf '%s' "$label" | tr ' /' '__').sh"
printf '%s\n' "$content" > "$f"
local stderr_file="$TMPDIR/$(basename "$f").stderr"
"$FORMATTER" "$f" >/dev/null 2>"$stderr_file"
local actual_exit=$?
local stderr_content
stderr_content="$(cat "$stderr_file")"
if [ "$actual_exit" -ne "$expected_exit" ]; then
fail "$label (exit)" "expected $expected_exit, got $actual_exit. stderr: $stderr_content"
return
fi
if [ -n "$pattern" ] && ! echo "$stderr_content" | grep -qE "$pattern"; then
fail "$label (pattern)" "expected pattern '$pattern', got: $stderr_content"
return
fi
pass "$label"
}
# Write a fixture and run the formatter. Assert:
# - exit code is 0
# - stderr does NOT contain the forbidden pattern
assert_clean() {
local label="$1"
local forbidden="$2"
local content="$3"
local f="$TMPDIR/$(printf '%s' "$label" | tr ' /' '__').sh"
printf '%s\n' "$content" > "$f"
local stderr_file="$TMPDIR/$(basename "$f").stderr"
"$FORMATTER" "$f" >/dev/null 2>"$stderr_file"
local actual_exit=$?
local stderr_content
stderr_content="$(cat "$stderr_file")"
if [ "$actual_exit" -ne 0 ]; then
fail "$label (exit)" "expected 0, got $actual_exit. stderr: $stderr_content"
return
fi
if [ -n "$forbidden" ] && echo "$stderr_content" | grep -qE "$forbidden"; then
fail "$label (forbidden pattern)" "should NOT match '$forbidden', got: $stderr_content"
return
fi
pass "$label"
}
# ===========================================================================
echo "=== SETE-1: shebanged script without set -e ==="
# ===========================================================================
# Positive: shebang, no set -e anywhere → must flag SETE-1, exit 1
assert_flagged "SETE-1 positive" 1 "SETE-1" \
"#!/usr/bin/env bash
echo hello
ls /nonexistent
echo world"
# Negative: shebang + set -euo pipefail → no SETE-1 finding, exit 0
assert_clean "SETE-1 negative (has set -euo pipefail)" "SETE-1" \
"#!/usr/bin/env bash
set -euo pipefail
echo hello
ls /nonexistent || true"
# Negative: shebang + set -e (short form) → no SETE-1 finding, exit 0
assert_clean "SETE-1 negative (has set -e)" "SETE-1" \
"#!/usr/bin/env bash
set -e
echo hello"
# Negative: sourced fragment (no shebang) without set -e → no SETE-1 finding
# `shell=bash` directive satisfies shellcheck's SC2148 ("add a shebang or shell directive")
assert_clean "SETE-1 negative (sourced fragment, no shebang)" "SETE-1" \
"# shellcheck shell=bash
echo hello
ls /nonexistent || true
echo world"
# ===========================================================================
echo ""
echo "=== SETE-2: sh/bash -c with multi-command string and no set -e ==="
# ===========================================================================
# Positive: sh -c with two ;-separated commands and no set -e → flag SETE-2
assert_flagged "SETE-2 positive (hugo mod get; mod tidy)" 1 "SETE-2" \
"#!/usr/bin/env bash
set -euo pipefail
sh -c \"hugo mod get github.com/foo/bar; hugo mod tidy\""
# Positive: bash -c with multiple ; commands, no set -e
assert_flagged "SETE-2 positive (bash -c cmd1; cmd2)" 1 "SETE-2" \
"#!/usr/bin/env bash
set -euo pipefail
bash -c \"echo a; echo b; echo c\""
# Negative: sh -c with single command → no SETE-2 finding
assert_clean "SETE-2 negative (single command)" "SETE-2" \
"#!/usr/bin/env bash
set -euo pipefail
sh -c \"hugo mod tidy\""
# Negative: sh -c with multi commands BUT set -e inside → no SETE-2 finding
assert_clean "SETE-2 negative (has set -e inside)" "SETE-2" \
"#!/usr/bin/env bash
set -euo pipefail
sh -c \"set -e; hugo mod get; hugo mod tidy\""
# ===========================================================================
echo ""
echo "=== LEAK-1: secret-named file read into echo/printf/die/error/fail ==="
# ===========================================================================
# Positive: die "$(cat "$KV_JSON_FILE")" — the canonical real case from the brief
assert_flagged "LEAK-1 positive (die with cat \$KV_JSON_FILE)" 1 "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
die \"kv put failed: \$(cat \"\$KV_JSON_FILE\")\""
# Positive: echo with TOKEN-named file read
assert_flagged "LEAK-1 positive (echo with \$GITHUB_TOKEN_FILE)" 1 "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
echo \"got: \$(cat \"\$GITHUB_TOKEN_FILE\")\""
# Positive: printf with PASSWORD-named file read
assert_flagged "LEAK-1 positive (printf with \$DB_PASSWORD_FILE)" 1 "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
printf 'db password was: %s\n' \"\$(< \"\$DB_PASSWORD_FILE\")\""
# Negative: die with no secret-named variable
assert_clean "LEAK-1 negative (regular error)" "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
die \"kv put failed: connection refused\""
# Negative: die with secret-named var but NOT read via cat/$() — just interpolated
assert_clean "LEAK-1 negative (var interpolated, not cat-substituted)" "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
RESULT_VAR=foo
die \"error code: \$RESULT_VAR\""
# Negative: secret-named var read via cat but NOT in echo/die/etc.
# Mark vars as used via export so shellcheck doesn't flag SC2034.
assert_clean "LEAK-1 negative (secret file read, not in echo)" "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
contents=\$(cat \"\$API_TOKEN_FILE\")
export contents"
# Negative: echo with non-secret variable name
assert_clean "LEAK-1 negative (echo with non-secret var)" "LEAK-1" \
"#!/usr/bin/env bash
set -euo pipefail
echo \"result file content: \$(cat \"\$RESULT_FILE\")\""
# ===========================================================================
echo ""
echo "=== Missing-tool warning (formatters/sh must NOT silently pass) ==="
# ===========================================================================
# When neither tool is on PATH, formatter should emit ONE warning line.
# Build a PATH that has bash + python3 but no shfmt or shellcheck, then run.
PATH_CLEAN="$(mktemp -d)"
ln -sf /usr/bin/bash "$PATH_CLEAN/bash"
ln -sf /usr/bin/python3 "$PATH_CLEAN/python3"
stderr_file="$TMPDIR/no-tools.stderr"
# Use a clean fixture — must not flag any SETE/LEAK.
printf '#!/usr/bin/env bash\nset -euo pipefail\necho hello\n' > "$TMPDIR/clean-fixture.sh"
PATH="$PATH_CLEAN" bash "$FORMATTER" "$TMPDIR/clean-fixture.sh" >/dev/null 2>"$stderr_file"
rc=$?
stderr_content="$(cat "$stderr_file")"
rm -rf "$PATH_CLEAN"
if [ "$rc" -eq 0 ] && echo "$stderr_content" | grep -qE 'formatters/sh: shfmt and shellcheck not installed'; then
pass "missing-tool warning emitted, exit 0"
else
fail "missing-tool warning" "expected exit 0 + warning line, got exit=$rc stderr=$stderr_content"
fi
# SETE-2 must see a MULTI-LINE `sh -c "..."`. That is the shape of the real
# sync-deps bug; the original per-line regex required both quotes on one line
# and missed it entirely.
f="$TMPDIR/sete2-multiline.sh"
printf '#!/usr/bin/env bash\nset -euo pipefail\ndocker run --rm img sh -c "\n hugo mod get a b c\n hugo mod tidy\n"\n' > "$f"
out="$(bash "$FORMATTER" "$f" 2>&1)"
if printf '%s' "$out" | grep -q "SETE-2"; then
pass "SETE-2 flags a multi-line sh -c with no set -e"
else
fail "SETE-2 MISSED the multi-line sh -c form (the real sync-deps bug); got: $out"
fi
f="$TMPDIR/sete2-multiline-ok.sh"
printf '#!/usr/bin/env bash\nset -euo pipefail\ndocker run --rm img sh -c "\n set -e\n hugo mod get a b c\n hugo mod tidy\n"\n' > "$f"
out="$(bash "$FORMATTER" "$f" 2>&1)"
if printf '%s' "$out" | grep -q "SETE-2"; then
fail "SETE-2 false-positived on a multi-line sh -c that DOES set -e"
else
pass "SETE-2 quiet when the multi-line sh -c sets -e"
fi
# ===========================================================================
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -eq 0 ]; then
echo "ALL_OK"
exit 0
fi
exit 1