From c238afbd4150ef53737f16e7dd84a565d2838ecc Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 23 Jul 2026 20:48:44 -0500 Subject: feat(languages): ship the missing python and typescript hooks The python and typescript bundles carried rules and a coverage script but no pre-commit hook, so any project installing one got no credential scan on commit. Both now ship all four components the README documents: the shared secret scan, a validate-on-edit hook, settings wiring, and a seed CLAUDE.md. Verified against a real repo: a commit carrying an AWS key is refused. install-lang now warns when a bundle is missing a documented component. That's the half that keeps this from recurring. Whoever adds the sixth bundle will forget something too, and today the installer prints success either way. Two things fell out of the build. node --check is unusable on TypeScript: it ignores --experimental-strip-types, so it rejects valid TS and accepts broken TS. The hook uses tsc filtered to syntactic diagnostics instead. And completing the bundles means every pair now collides on settings.json and pre-commit, so no two compose without FORCE=1. The earlier "bundles already compose" reading rested on these two being incomplete. Filed for a real decision; clock-panel is the project that wants it. Also stamps :LAST_REVIEWED: at task creation, with a lint checker to catch misses. Writing a task is reviewing it, so leaving the stamp off pushed every fresh task to the top of the next review batch to be re-derived by someone with less context than its author had. Tonight's sweep sent the staleness count from 13 to 22 while the list got more accurate. --- languages/python/tests/pre-commit.bats | 138 +++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 languages/python/tests/pre-commit.bats (limited to 'languages/python/tests/pre-commit.bats') diff --git a/languages/python/tests/pre-commit.bats b/languages/python/tests/pre-commit.bats new file mode 100644 index 0000000..1ac82ee --- /dev/null +++ b/languages/python/tests/pre-commit.bats @@ -0,0 +1,138 @@ +#!/usr/bin/env bats +# +# Tests for languages/python/githooks/pre-commit — the secret scan plus +# syntax/lint gate that runs on staged Python files. +# +# The secret scan is the security-critical half and is language-independent, so +# it gets the same coverage here as in the bash bundle: a real key blocks, a +# clean diff passes, and the case-sensitivity split that keeps base64 blobs from +# false-positiving is exercised directly. +# +# Each test builds a throwaway git repo, stages content, and runs the hook from +# inside it — the hook reads `git diff --cached`, so a real index is required. + +HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/githooks/pre-commit" + +setup() { + TEST_DIR="$(mktemp -d -t pre-commit-py-bats.XXXXXX)" + cd "$TEST_DIR" || exit 1 + git init -q . + git config user.email t@example.com + git config user.name Test + # A base commit so `git diff --cached` has a parent to diff against. + echo "seed" > seed.txt + git add seed.txt + git commit -qm seed +} + +teardown() { + cd / || true + rm -rf "$TEST_DIR" +} + +# ---- Normal ---------------------------------------------------------- + +@test "pre-commit(py): a clean staged Python file passes (exit 0)" { + printf 'def f(x):\n return x + 1\n' > ok.py + git add ok.py + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(py): an empty staging area passes (exit 0)" { + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +# ---- Error: the secret scan ------------------------------------------ + +@test "pre-commit(py): an AWS key in a staged file blocks (exit 1)" { + printf 'KEY = "AKIAIOSFODNN7EXAMPLE"\n' > conf.py + git add conf.py + run bash "$HOOK" + [ "$status" -eq 1 ] + [[ "$output" == *"potential secret"* ]] +} + +@test "pre-commit(py): an sk- style token blocks (exit 1)" { + printf 'TOKEN = "sk-abcdefghijklmnopqrstuvwxyz0123"\n' > conf.py + git add conf.py + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(py): a quoted api_key assignment blocks (exit 1)" { + printf 'api_key = "abcdefghijklmnopqrstuvwxyz"\n' > conf.py + git add conf.py + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(py): a private-key header blocks (exit 1)" { + printf 'PEM = """-----BEGIN RSA PRIVATE KEY-----"""\n' > conf.py + git add conf.py + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +# ---- Boundary: the case-sensitivity split ---------------------------- + +@test "pre-commit(py): a mixed-case base64 blob does NOT false-positive" { + # The AWS pattern is uppercase-only by design. Under -i it would match any + # 20-char mixed-case run, which random base64 hits often enough to block + # real commits. This is the regression test for that split. + printf 'BLOB = "AKIAbcdefGHIJklmnOPqr0123456789abcdefGHIJ"\n' > data.py + git add data.py + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(py): a short quoted password value does NOT block" { + # The keyword patterns require 16+ chars, so a placeholder stays quiet. + printf 'password = "short"\n' > conf.py + git add conf.py + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(py): a secret only in a REMOVED line does not block" { + printf 'KEY = "AKIAIOSFODNN7EXAMPLE"\n' > conf.py + git add conf.py + git commit -qm "add key" + rm conf.py + git add -A + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +# ---- Error: the syntax gate ------------------------------------------ + +@test "pre-commit(py): a staged Python syntax error blocks (exit 1)" { + printf 'def f(:\n return 1\n' > bad.py + git add bad.py + run bash "$HOOK" + [ "$status" -eq 1 ] + [[ "$output" == *"syntax"* ]] +} + +@test "pre-commit(py): a .pyi stub with a syntax error blocks (exit 1)" { + printf 'def f( -> int: ...\n' > bad.pyi + git add bad.pyi + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(py): a broken NON-Python file does not trip the syntax gate" { + printf 'this is (((not python\n' > notes.txt + git add notes.txt + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(py): the syntax gate leaves no __pycache__ in the repo" { + printf 'def f():\n return 1\n' > ok.py + git add ok.py + run bash "$HOOK" + [ "$status" -eq 0 ] + [ ! -d __pycache__ ] +} -- cgit v1.2.3