diff options
Diffstat (limited to 'languages/typescript')
| -rw-r--r-- | languages/typescript/CLAUDE.md | 82 | ||||
| -rwxr-xr-x | languages/typescript/claude/hooks/validate-typescript.sh | 94 | ||||
| -rw-r--r-- | languages/typescript/claude/settings.json | 80 | ||||
| -rwxr-xr-x | languages/typescript/githooks/pre-commit | 92 | ||||
| -rw-r--r-- | languages/typescript/tests/pre-commit.bats | 150 | ||||
| -rw-r--r-- | languages/typescript/tests/validate-typescript.bats | 125 |
6 files changed, 623 insertions, 0 deletions
diff --git a/languages/typescript/CLAUDE.md b/languages/typescript/CLAUDE.md new file mode 100644 index 0000000..1794115 --- /dev/null +++ b/languages/typescript/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +## Project + +TypeScript/JavaScript project. Customize this section with your own +description, layout, and conventions. + +**Typical layout:** +- `src/` — source modules +- `tests/` or `*.test.ts` beside the source — test files +- `package.json` — scripts and dependencies +- `tsconfig.json` — compiler options + +## Build & Test Commands + +If the project has a Makefile, document targets here. Common pattern: + +```bash +make test # run the test suite +make coverage # suite + coverage report +make typecheck # tsc --noEmit across the project +make lint # eslint +make build # production build +``` + +Direct equivalents: `npm test`, `npx tsc --noEmit`, `npx eslint src/`, +`npx prettier --check .`, `node --test`. + +## Language Rules + +See rule files in `.claude/rules/`: +- `typescript-testing.md` — test conventions and mocking discipline +- `verification.md` — verify-before-claim-done discipline + +## Git Workflow + +Commit conventions: see `.claude/rules/commits.md` (author identity, +no AI attribution, message format). + +Pre-commit hook in `githooks/` scans for secrets and parse-checks staged TS/JS. +Activate on a fresh clone with `git config core.hooksPath githooks`. + +## Problem-Solving Approach + +Investigate before fixing. When diagnosing a bug: +1. Read the relevant module and trace what actually happens +2. Identify the root cause, not a surface symptom +3. Write a failing test that captures the correct behavior +4. Fix, then re-run tests + +## Testing Discipline + +TDD is the default: write a failing test before any implementation. If you can't +write the test, you don't yet understand the change. Details in +`.claude/rules/typescript-testing.md`. + +## Editing Discipline + +A PostToolUse hook parse-checks every TS/JS file after Edit/Write/MultiEdit and +blocks on a syntax error. It covers `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, +`.jsx`, `.mjs`, and `.cjs`. + +Two checkers, because one tool can't do both jobs: `node --check` for +JavaScript, `tsc` filtered to syntax diagnostics for TypeScript. Do not +substitute `node --check` for the TypeScript path — it ignores +`--experimental-strip-types`, so it rejects valid TypeScript and accepts broken +TypeScript (measured on node v26.4.0). + +Full type checking is not enforced by the hook: it needs the whole project graph +and its dependencies resolved, which is a build-scale operation rather than a +per-keystroke one. Run it via `make typecheck`. Formatting is likewise not +enforced; adopt a style per project in the project's own config. + +## What Not to Do + +- Don't add features beyond what was asked +- Don't refactor surrounding code when fixing a bug +- Don't reach for `any` to silence a type error — narrow the type instead +- Don't use `==` where `===` is meant +- Don't add comments to code you didn't change +- Don't commit `.env` files, credentials, or API keys — the pre-commit hook + catches common patterns but isn't a substitute for care diff --git a/languages/typescript/claude/hooks/validate-typescript.sh b/languages/typescript/claude/hooks/validate-typescript.sh new file mode 100755 index 0000000..b76f1df --- /dev/null +++ b/languages/typescript/claude/hooks/validate-typescript.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Validate TypeScript/JavaScript files after Edit/Write/MultiEdit. +# PostToolUse hook: receives tool-call JSON on stdin. +# +# On success: exit 0 silent. +# On failure: emit JSON with hookSpecificOutput.additionalContext so Claude +# sees a structured error in its context, THEN exit 2 to block the tool +# pipeline. stderr still echoes the error for terminal visibility. +# +# Gate: parseability. A file that doesn't parse is never worth passing on. +# Full type checking is deliberately NOT enforced here — it needs the whole +# project graph and its dependencies resolved, which is a build-scale +# operation, not a per-keystroke one. A type error that parses cleanly passes +# this hook; `make typecheck` / `tsc --noEmit` over the project owns it. +# +# Formatting (prettier) is also out: a project picks its own style, so blocking +# on an unconfigured default would impose a contested choice. +# +# Two checkers, because one tool can't do both jobs: +# +# .js/.jsx/.mjs/.cjs → `node --check`, a straight parse. +# .ts/.tsx/.mts/.cts → `tsc`, filtered to syntax-category diagnostics. +# +# `node --check` must NOT be used on TypeScript. It ignores +# --experimental-strip-types, so it is wrong in *both* directions: it rejects +# valid TS (an `interface` declaration reads as a syntax error) and accepts +# broken TS (a genuinely unparseable file exits 0). Measured on node v26.4.0, +# 2026-07-23. tsc is the only correct parser for these extensions. +# +# The tsc call is filtered to TS1xxx codes, which is TypeScript's syntactic +# diagnostic range; TS2xxx and up are semantic (type) errors and are out of +# scope by the paragraph above. Without the filter this hook would block every +# unresolved import in a file whose dependencies aren't installed yet. + +set -u + +# Emit a JSON failure payload and exit 2. Arguments: +# $1 — short failure type (e.g. "TYPESCRIPT SYNTAX ERROR") +# $2 — file path +# $3 — tool output (error body) +fail_json() { + local ctx + ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \ + | jq -Rs .)" + cat <<EOF +{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}} +EOF + printf '%s: %s\n%s\n' "$1" "$2" "$3" >&2 + exit 2 +} + +f="$(jq -r '.tool_input.file_path // .tool_response.filePath // empty')" +[ -z "$f" ] && exit 0 +[ -f "$f" ] || exit 0 + +# Classify by extension. Match on the basename, not the full path — a parent +# dir can carry a dot (e.g. my.project/) and confuse a path-wide match. +kind="" +base="${f##*/}" +case "$base" in + *.ts | *.tsx | *.mts | *.cts) kind="ts" ;; + *.js | *.jsx | *.mjs | *.cjs) kind="js" ;; + *) exit 0 ;; +esac + +if [ "$kind" = "js" ]; then + command -v node >/dev/null 2>&1 || exit 0 + if ! out="$(node --check "$f" 2>&1)"; then + fail_json "JAVASCRIPT SYNTAX ERROR" "$f" "$out" + fi + exit 0 +fi + +# TypeScript. Prefer a project-local tsc so the project's own version decides, +# falling back to one on PATH. +tsc_bin="" +if [ -x "./node_modules/.bin/tsc" ]; then + tsc_bin="./node_modules/.bin/tsc" +elif command -v tsc >/dev/null 2>&1; then + tsc_bin="tsc" +else + exit 0 # no TypeScript compiler available — don't block the edit +fi + +# --moduleDetection force so a file with no import/export still parses as a +# module rather than tripping global-scope collisions against lib types. +out="$("$tsc_bin" --noEmit --skipLibCheck --target es2022 --moduleDetection force "$f" 2>&1 || true)" +syntax_errors="$(printf '%s\n' "$out" | grep -E 'error TS1[0-9]{3}:' || true)" + +if [ -n "$syntax_errors" ]; then + fail_json "TYPESCRIPT SYNTAX ERROR" "$f" "$syntax_errors" +fi + +exit 0 diff --git a/languages/typescript/claude/settings.json b/languages/typescript/claude/settings.json new file mode 100644 index 0000000..f4c9211 --- /dev/null +++ b/languages/typescript/claude/settings.json @@ -0,0 +1,80 @@ +{ + "attribution": { + "commit": "", + "pr": "" + }, + "permissions": { + "allow": [ + "Bash(make)", + "Bash(make help)", + "Bash(make targets)", + "Bash(make test)", + "Bash(make test *)", + "Bash(make lint)", + "Bash(make fmt)", + "Bash(make coverage)", + "Bash(make coverage-summary)", + "Bash(make typecheck)", + "Bash(make build)", + "Bash(npm test)", + "Bash(npm test *)", + "Bash(npm run *)", + "Bash(npm ci)", + "Bash(npm ls *)", + "Bash(node --check *)", + "Bash(node --test *)", + "Bash(node --version)", + "Bash(tsc --noEmit *)", + "Bash(npx tsc --noEmit *)", + "Bash(eslint *)", + "Bash(prettier --check *)", + "Bash(git status)", + "Bash(git status *)", + "Bash(git diff)", + "Bash(git diff *)", + "Bash(git log)", + "Bash(git log *)", + "Bash(git show)", + "Bash(git show *)", + "Bash(git blame *)", + "Bash(git branch)", + "Bash(git branch -v)", + "Bash(git branch -a)", + "Bash(git branch --list *)", + "Bash(git remote)", + "Bash(git remote -v)", + "Bash(git remote show *)", + "Bash(git ls-files *)", + "Bash(git rev-parse *)", + "Bash(git cat-file *)", + "Bash(git stash list)", + "Bash(git stash show *)", + "Bash(jq *)", + "Bash(date)", + "Bash(date *)", + "Bash(which *)", + "Bash(file *)", + "Bash(ls)", + "Bash(ls *)", + "Bash(wc *)", + "Bash(du *)", + "Bash(readlink *)", + "Bash(realpath *)", + "Bash(basename *)", + "Bash(dirname *)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-typescript.sh" + } + ] + } + ] + } +} diff --git a/languages/typescript/githooks/pre-commit b/languages/typescript/githooks/pre-commit new file mode 100755 index 0000000..1628080 --- /dev/null +++ b/languages/typescript/githooks/pre-commit @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Pre-commit hook: secret scan + syntax check on staged TypeScript/JavaScript files. +# Use `git commit --no-verify` to bypass for confirmed false positives. + +set -u + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" || exit 1 + +# --- 1. Secret scan --- +# Patterns for common credentials. Scans only added lines in the staged diff. +# +# Two passes because case-sensitivity differs. AWS keys are uppercase, sk- keys +# lowercase, PEM headers fixed, so those match case-SENSITIVELY: under -i, +# AKIA[0-9A-Z]{16} matches any mixed-case 20-char run, which random base64 in an +# embedded image blob hits ~6% of the time per 100KB and blocks real commits. +# Only the keyword=value patterns need -i. +SECRET_PATTERNS_CS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9_-]{20,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP)( PRIVATE)?( KEY| KEY BLOCK)?-----)' +SECRET_PATTERNS_CI='(api[_-]?key|api[_-]?secret|auth[_-]?token|secret[_-]?key|bearer[_-]?token|access[_-]?token|password)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"']{16,}["'"'"']' + +added_lines="$(git diff --cached -U0 --diff-filter=AM \ + | grep '^+' | grep -v '^+++' || true)" + +cs_hits="$(printf '%s\n' "$added_lines" | grep -nE "$SECRET_PATTERNS_CS" || true)" +ci_hits="$(printf '%s\n' "$added_lines" | grep -niE "$SECRET_PATTERNS_CI" || true)" +# awk dedupes lines both passes matched, keeping first-seen order. +secret_hits="$(printf '%s\n%s' "$cs_hits" "$ci_hits" \ + | grep -v '^[[:space:]]*$' | awk '!seen[$0]++' || true)" + +if [ -n "$secret_hits" ]; then + echo "pre-commit: potential secret in staged changes:" >&2 + echo "$secret_hits" >&2 + echo "" >&2 + echo "Review the lines above. If this is a false positive (test fixture, documentation)," >&2 + echo "bypass with: git commit --no-verify" >&2 + exit 1 +fi + +# --- 2. Syntax check on staged TS/JS files --- +# Two checkers, because one tool can't do both jobs. `node --check` ignores +# --experimental-strip-types, so on TypeScript it is wrong in BOTH directions: +# it rejects valid TS (an `interface` reads as a syntax error) and accepts +# broken TS. Measured on node v26.4.0, 2026-07-23. tsc is the only correct +# parser for .ts; node is correct and much faster for .js. +staged_js="$(git diff --cached --name-only --diff-filter=AM \ + | grep -E '\.(js|jsx|mjs|cjs)$' || true)" +staged_ts="$(git diff --cached --name-only --diff-filter=AM \ + | grep -E '\.(ts|tsx|mts|cts)$' || true)" + +failed="" + +if [ -n "$staged_js" ] && command -v node >/dev/null 2>&1; then + while IFS= read -r f; do + [ -z "$f" ] && continue + [ -f "$f" ] || continue + if ! node --check "$f" >/dev/null 2>&1; then + failed="${failed}${f}"$'\n' + fi + done <<< "$staged_js" +fi + +if [ -n "$staged_ts" ]; then + tsc_bin="" + if [ -x "./node_modules/.bin/tsc" ]; then + tsc_bin="./node_modules/.bin/tsc" + elif command -v tsc >/dev/null 2>&1; then + tsc_bin="tsc" + fi + + if [ -n "$tsc_bin" ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + [ -f "$f" ] || continue + # Filter to TS1xxx, TypeScript's syntactic diagnostic range. TS2xxx and + # up are type errors, which need the whole project graph and are the + # build's job, not this hook's. + out="$("$tsc_bin" --noEmit --skipLibCheck --target es2022 \ + --moduleDetection force "$f" 2>&1 || true)" + if printf '%s\n' "$out" | grep -qE 'error TS1[0-9]{3}:'; then + failed="${failed}${f}"$'\n' + fi + done <<< "$staged_ts" + fi +fi + +if [ -n "$failed" ]; then + printf 'pre-commit: syntax errors in staged files:\n\n%s\n' "$failed" >&2 + echo "Fix the parse errors above, then re-stage." >&2 + exit 1 +fi + +exit 0 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 ] +} 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 ] +} |
