diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-23 20:48:44 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-23 20:48:44 -0500 |
| commit | c238afbd4150ef53737f16e7dd84a565d2838ecc (patch) | |
| tree | 588a25dc318cb854c57d8146639d400674baed18 /languages/typescript/tests/validate-typescript.bats | |
| parent | 10ea44b6de3be1872f7f0bd4501ccf3878105bc4 (diff) | |
| download | rulesets-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/validate-typescript.bats')
| -rw-r--r-- | languages/typescript/tests/validate-typescript.bats | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/languages/typescript/tests/validate-typescript.bats b/languages/typescript/tests/validate-typescript.bats new file mode 100644 index 0000000..c5da5d4 --- /dev/null +++ b/languages/typescript/tests/validate-typescript.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# +# Tests for languages/typescript/claude/hooks/validate-typescript.sh — the +# PostToolUse hook that syntax-checks edited TS/JS 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 needs node, so those tests skip when node is absent. Full +# type checking is deliberately out of scope for the hook (it needs the whole +# project graph), so a type error that is syntactically valid must pass. + +HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/claude/hooks/validate-typescript.sh" + +setup() { + TEST_DIR="$(mktemp -d -t validate-ts-bats.XXXXXX)" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +payload() { + printf '{"tool_input": {"file_path": "%s"}}' "$1" +} + +# ---- Normal ---------------------------------------------------------- + +@test "validate-typescript: a clean .ts file passes silently (exit 0)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f(x: number): number {\n return x + 1;\n}\n' > "$TEST_DIR/clean.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.ts")" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "validate-typescript: a clean .js file passes silently (exit 0)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f(x) {\n return x + 1;\n}\n' > "$TEST_DIR/clean.js" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.js")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a .tsx file is validated too" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export const A = 1;\n' > "$TEST_DIR/clean.tsx" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.tsx")" + [ "$status" -eq 0 ] +} + +# ---- Error ----------------------------------------------------------- + +@test "validate-typescript: a syntax error blocks (exit 2, names the failure)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f( {\n return 1;\n}\n' > "$TEST_DIR/bad.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.ts")" + [ "$status" -eq 2 ] + [[ "$output" == *"SYNTAX"* ]] +} + +@test "validate-typescript: the block payload is valid JSON carrying the context" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'const x = ;\n' > "$TEST_DIR/bad.js" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.js")" + [ "$status" -eq 2 ] + echo "$output" | head -1 | jq -e '.hookSpecificOutput.hookEventName == "PostToolUse"' +} + +# ---- Boundary -------------------------------------------------------- + +@test "validate-typescript: a type error that parses is NOT blocked (out of scope)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + # Assigning a string to a number is a type error, not a syntax error. The + # hook checks parseability only; tsc over the project graph owns this. + printf 'const n: number = "not a number";\nexport { n };\n' > "$TEST_DIR/typeerr.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/typeerr.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: TS-only syntax in a .ts file parses (not read as JS)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + # Interfaces and type annotations are invalid JS. Stripping types must happen + # before the parse, or every real .ts file would be reported as broken. + printf 'interface P { a: string }\nexport const p: P = { a: "x" };\n' > "$TEST_DIR/types.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/types.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a non-TS/JS file is ignored (exit 0)" { + printf 'not javascript at all (((\n' > "$TEST_DIR/notes.txt" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/notes.txt")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a .json file is ignored (exit 0)" { + printf '{"a": 1}\n' > "$TEST_DIR/data.json" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/data.json")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: empty file_path is a no-op (exit 0)" { + run bash "$HOOK" <<< '{"tool_input": {}}' + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a missing file is a no-op (exit 0)" { + run bash "$HOOK" <<< "$(payload "$TEST_DIR/does-not-exist.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: an empty .ts file passes" { + command -v node >/dev/null 2>&1 || skip "node not installed" + : > "$TEST_DIR/empty.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/empty.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a file in a dotted parent dir is still matched" { + command -v node >/dev/null 2>&1 || skip "node not installed" + mkdir -p "$TEST_DIR/my.project" + printf 'const x = ;\n' > "$TEST_DIR/my.project/bad.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/my.project/bad.ts")" + [ "$status" -eq 2 ] +} |
