aboutsummaryrefslogtreecommitdiff
path: root/languages/python/tests/validate-python.bats
diff options
context:
space:
mode:
Diffstat (limited to 'languages/python/tests/validate-python.bats')
-rw-r--r--languages/python/tests/validate-python.bats117
1 files changed, 117 insertions, 0 deletions
diff --git a/languages/python/tests/validate-python.bats b/languages/python/tests/validate-python.bats
new file mode 100644
index 0000000..b5e4957
--- /dev/null
+++ b/languages/python/tests/validate-python.bats
@@ -0,0 +1,117 @@
+#!/usr/bin/env bats
+#
+# Tests for languages/python/claude/hooks/validate-python.sh — the PostToolUse
+# hook that syntax-checks edited Python 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 syntax gate is python3's own compiler, which is present wherever the hook
+# can meaningfully run, so those tests never skip. The lint gate (ruff) is
+# optional and its tests skip when it's absent, matching the bash bundle's
+# treatment of shellcheck.
+
+HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/claude/hooks/validate-python.sh"
+
+setup() {
+ TEST_DIR="$(mktemp -d -t validate-python-bats.XXXXXX)"
+}
+
+teardown() {
+ rm -rf "$TEST_DIR"
+}
+
+payload() {
+ printf '{"tool_input": {"file_path": "%s"}}' "$1"
+}
+
+# ---- Normal ----------------------------------------------------------
+
+@test "validate-python: a clean .py file passes silently (exit 0)" {
+ printf 'def f(x):\n return x + 1\n' > "$TEST_DIR/clean.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.py")"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "validate-python: a .pyi stub is validated too" {
+ printf 'def f(x: int) -> int: ...\n' > "$TEST_DIR/clean.pyi"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.pyi")"
+ [ "$status" -eq 0 ]
+}
+
+# ---- Error -----------------------------------------------------------
+
+@test "validate-python: a syntax error blocks (exit 2, names the failure)" {
+ printf 'def f(:\n return 1\n' > "$TEST_DIR/bad.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.py")"
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"SYNTAX"* ]]
+}
+
+@test "validate-python: the block payload is valid JSON carrying the context" {
+ printf 'def f(:\n' > "$TEST_DIR/bad.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.py")"
+ [ "$status" -eq 2 ]
+ # The first line of stdout must parse as JSON and carry the hook event name.
+ echo "$output" | head -1 | jq -e '.hookSpecificOutput.hookEventName == "PostToolUse"'
+}
+
+@test "validate-python: a ruff violation blocks when ruff is installed" {
+ command -v ruff >/dev/null 2>&1 || skip "ruff not installed"
+ # F821: reference to an undefined name — syntactically valid, lint-caught.
+ printf 'def f():\n return undefined_name\n' > "$TEST_DIR/lint.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/lint.py")"
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"RUFF"* ]]
+}
+
+# ---- Boundary --------------------------------------------------------
+
+@test "validate-python: a non-Python file is ignored (exit 0)" {
+ printf 'not python at all (((\n' > "$TEST_DIR/notes.txt"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/notes.txt")"
+ [ "$status" -eq 0 ]
+}
+
+@test "validate-python: an extensionless file with a python shebang is validated" {
+ printf '#!/usr/bin/env python3\ndef f(:\n' > "$TEST_DIR/cli-tool"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/cli-tool")"
+ [ "$status" -eq 2 ]
+}
+
+@test "validate-python: an extensionless non-python file is ignored (exit 0)" {
+ printf '#!/usr/bin/env bash\necho hi\n' > "$TEST_DIR/shell-tool"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/shell-tool")"
+ [ "$status" -eq 0 ]
+}
+
+@test "validate-python: a dotted parent directory does not misfire the extension test" {
+ mkdir -p "$TEST_DIR/my.project"
+ printf '#!/usr/bin/env python3\ndef f(:\n' > "$TEST_DIR/my.project/cli-tool"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/my.project/cli-tool")"
+ [ "$status" -eq 2 ]
+}
+
+@test "validate-python: empty file_path is a no-op (exit 0)" {
+ run bash "$HOOK" <<< '{"tool_input": {}}'
+ [ "$status" -eq 0 ]
+}
+
+@test "validate-python: a missing file is a no-op (exit 0)" {
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/does-not-exist.py")"
+ [ "$status" -eq 0 ]
+}
+
+@test "validate-python: an empty .py file passes (valid, compiles to nothing)" {
+ : > "$TEST_DIR/empty.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/empty.py")"
+ [ "$status" -eq 0 ]
+}
+
+@test "validate-python: compiling leaves no __pycache__ beside the file" {
+ printf 'def f():\n return 1\n' > "$TEST_DIR/clean.py"
+ run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.py")"
+ [ "$status" -eq 0 ]
+ [ ! -d "$TEST_DIR/__pycache__" ]
+}