blob: b5e495735e8ab2e8233f60f9fc9a4b3896c1e7ff (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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__" ]
}
|