aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript/tests/pre-commit.bats
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-23 20:48:44 -0500
committerCraig Jennings <c@cjennings.net>2026-07-23 20:48:44 -0500
commitc238afbd4150ef53737f16e7dd84a565d2838ecc (patch)
tree588a25dc318cb854c57d8146639d400674baed18 /languages/typescript/tests/pre-commit.bats
parent10ea44b6de3be1872f7f0bd4501ccf3878105bc4 (diff)
downloadrulesets-c238afbd4150ef53737f16e7dd84a565d2838ecc.tar.gz
rulesets-c238afbd4150ef53737f16e7dd84a565d2838ecc.zip
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.
Diffstat (limited to 'languages/typescript/tests/pre-commit.bats')
-rw-r--r--languages/typescript/tests/pre-commit.bats150
1 files changed, 150 insertions, 0 deletions
diff --git a/languages/typescript/tests/pre-commit.bats b/languages/typescript/tests/pre-commit.bats
new file mode 100644
index 0000000..5519baa
--- /dev/null
+++ b/languages/typescript/tests/pre-commit.bats
@@ -0,0 +1,150 @@
+#!/usr/bin/env bats
+#
+# Tests for languages/typescript/githooks/pre-commit — the secret scan plus
+# syntax/lint gate that runs on staged TS/JS 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-ts-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(ts): a clean staged JS file passes (exit 0)" {
+ printf 'export const f = (x) => x + 1;\n' > ok.js
+ git add ok.js
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+@test "pre-commit(ts): an empty staging area passes (exit 0)" {
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+# ---- Error: the secret scan ------------------------------------------
+
+@test "pre-commit(ts): an AWS key in a staged file blocks (exit 1)" {
+ printf 'const KEY = "AKIAIOSFODNN7EXAMPLE";\n' > conf.ts
+ git add conf.ts
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"potential secret"* ]]
+}
+
+@test "pre-commit(ts): an sk- style token blocks (exit 1)" {
+ printf 'const TOKEN = "sk-abcdefghijklmnopqrstuvwxyz0123";\n' > conf.ts
+ git add conf.ts
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+}
+
+@test "pre-commit(ts): a quoted api_key assignment blocks (exit 1)" {
+ printf 'const api_key = "abcdefghijklmnopqrstuvwxyz";\n' > conf.ts
+ git add conf.ts
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+}
+
+@test "pre-commit(ts): a private-key header blocks (exit 1)" {
+ printf 'const PEM = "-----BEGIN RSA PRIVATE KEY-----";\n' > conf.ts
+ git add conf.ts
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+}
+
+# ---- Boundary: the case-sensitivity split ----------------------------
+
+@test "pre-commit(ts): 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 'const BLOB = "AKIAbcdefGHIJklmnOPqr0123456789abcdefGHIJ";\n' > data.ts
+ git add data.ts
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+@test "pre-commit(ts): a short quoted password value does NOT block" {
+ # The keyword patterns require 16+ chars, so a placeholder stays quiet.
+ printf 'const password = "short";\n' > conf.ts
+ git add conf.ts
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+@test "pre-commit(ts): a secret only in a REMOVED line does not block" {
+ printf 'const KEY = "AKIAIOSFODNN7EXAMPLE";\n' > conf.ts
+ git add conf.ts
+ git commit -qm "add key"
+ rm conf.ts
+ git add -A
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+# ---- Error: the syntax gate ------------------------------------------
+
+@test "pre-commit(ts): a staged JS syntax error blocks (exit 1)" {
+ command -v node >/dev/null 2>&1 || skip "node not installed"
+ printf 'const x = ;\n' > bad.js
+ git add bad.js
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"syntax"* ]]
+}
+
+@test "pre-commit(ts): a staged TS syntax error blocks (exit 1)" {
+ command -v tsc >/dev/null 2>&1 || skip "tsc not installed"
+ printf 'export function f( {\n return 1;\n}\n' > bad.ts
+ git add bad.ts
+ run bash "$HOOK"
+ [ "$status" -eq 1 ]
+}
+
+@test "pre-commit(ts): valid TS-only syntax is NOT read as broken JS" {
+ command -v tsc >/dev/null 2>&1 || skip "tsc not installed"
+ # The regression guard for the node --check trap: `node --check` rejects
+ # valid TypeScript, so using it on .ts would block every real commit.
+ printf 'interface P { a: string }\nexport const p: P = { a: "x" };\n' > types.ts
+ git add types.ts
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+@test "pre-commit(ts): a TYPE error that parses does not block (out of scope)" {
+ command -v tsc >/dev/null 2>&1 || skip "tsc not installed"
+ printf 'const n: number = "nope";\nexport { n };\n' > typeerr.ts
+ git add typeerr.ts
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}
+
+@test "pre-commit(ts): a broken NON-TS/JS file does not trip the syntax gate" {
+ printf 'this is (((not javascript\n' > notes.txt
+ git add notes.txt
+ run bash "$HOOK"
+ [ "$status" -eq 0 ]
+}