#!/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 ] }