diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/install-lang.sh | 19 | ||||
| -rwxr-xr-x | scripts/lint.sh | 3 | ||||
| -rw-r--r-- | scripts/tests/ai-wrap-teardown-hook.bats | 101 | ||||
| -rw-r--r-- | scripts/tests/install-lang.bats | 47 |
4 files changed, 167 insertions, 3 deletions
diff --git a/scripts/install-lang.sh b/scripts/install-lang.sh index 0fc9ea8..2f38fcd 100755 --- a/scripts/install-lang.sh +++ b/scripts/install-lang.sh @@ -66,13 +66,26 @@ if [ -d "$SRC/githooks" ]; then fi fi -# 3. CLAUDE.md — seed on first install, don't overwrite unless FORCE=1 +# 3. CLAUDE.md — seed on first install, don't overwrite unless FORCE=1. +# Prefer the bundle's own template; fall back to the language-neutral +# default so a bundle that ships none still seeds an accurate, non- +# mislabeling header instead of nothing. The default names no language, +# so multi-bundle and wrong-bundle installs don't inherit a false header. +CLAUDE_SRC="" +CLAUDE_KIND="" if [ -f "$SRC/CLAUDE.md" ]; then + CLAUDE_SRC="$SRC/CLAUDE.md" + CLAUDE_KIND="$LANG" +elif [ -f "$REPO_ROOT/languages/default-CLAUDE.md" ]; then + CLAUDE_SRC="$REPO_ROOT/languages/default-CLAUDE.md" + CLAUDE_KIND="language-neutral default" +fi +if [ -n "$CLAUDE_SRC" ]; then if [ -f "$PROJECT/CLAUDE.md" ] && [ "$FORCE" != "1" ]; then echo " [skip] CLAUDE.md already exists (use FORCE=1 to overwrite)" else - cp "$SRC/CLAUDE.md" "$PROJECT/CLAUDE.md" - echo " [ok] CLAUDE.md installed" + cp "$CLAUDE_SRC" "$PROJECT/CLAUDE.md" + echo " [ok] CLAUDE.md installed ($CLAUDE_KIND)" fi fi diff --git a/scripts/lint.sh b/scripts/lint.sh index ae30aa5..61a27a1 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -99,6 +99,9 @@ for claude_md in languages/*/CLAUDE.md; do check_md_heading "$claude_md" done +# Language-neutral default CLAUDE.md (install-lang's fallback when a bundle ships none) +[ -f languages/default-CLAUDE.md ] && check_md_heading languages/default-CLAUDE.md + # Hook scripts for h in languages/*/claude/hooks/*.sh languages/*/githooks/*; do [ -f "$h" ] || continue diff --git a/scripts/tests/ai-wrap-teardown-hook.bats b/scripts/tests/ai-wrap-teardown-hook.bats new file mode 100644 index 0000000..05c49f1 --- /dev/null +++ b/scripts/tests/ai-wrap-teardown-hook.bats @@ -0,0 +1,101 @@ +#!/usr/bin/env bats +# hooks/ai-wrap-teardown.sh — Stop hook that tears down the ai-term session +# (or powers off) after a wrap-up, gated on a sentinel wrap-it-up drops. On a +# normal stop (no sentinel) it is a silent no-op. The emacsclient call is +# stubbed here so the test records the elisp form without a live daemon. + +setup() { + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + SCRIPT="$REPO_ROOT/hooks/ai-wrap-teardown.sh" + TMPDIR_T="$(mktemp -d)" + PROJ="proj-$$-$BATS_TEST_NUMBER" # unique so /tmp sentinels don't collide + CWD="$TMPDIR_T/$PROJ" + mkdir -p "$CWD" + TEARDOWN_SENTINEL="/tmp/ai-wrap-teardown-${PROJ}" + SHUTDOWN_SENTINEL="/tmp/ai-wrap-shutdown-${PROJ}" + + # Stub emacsclient on PATH: record the elisp form it was called with. + BIN="$TMPDIR_T/bin" + mkdir -p "$BIN" + EC_LOG="$TMPDIR_T/emacsclient.log" + cat >"$BIN/emacsclient" <<EOF +#!/usr/bin/env bash +# args: -e <form> +shift # drop -e +printf '%s\n' "\$1" >> "$EC_LOG" +EOF + chmod +x "$BIN/emacsclient" +} + +teardown() { + rm -rf "$TMPDIR_T" + rm -f "$TEARDOWN_SENTINEL" "$SHUTDOWN_SENTINEL" +} + +run_hook() { + # invoke with the stubbed emacsclient on PATH, feeding Stop-hook JSON + printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="$BIN:$PATH" bash "$SCRIPT" +} + +@test "no sentinel: silent no-op, emacsclient never called" { + run run_hook + [ "$status" -eq 0 ] + [ -z "$output" ] + [ ! -f "$EC_LOG" ] +} + +@test "teardown sentinel: calls cj/ai-term-quit with the project basename" { + : > "$TEARDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-quit \"$PROJ\"" "$EC_LOG" +} + +@test "teardown sentinel is removed after firing" { + : > "$TEARDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + [ ! -f "$TEARDOWN_SENTINEL" ] +} + +@test "shutdown sentinel: calls cj/ai-term-shutdown-countdown" { + : > "$SHUTDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-shutdown-countdown" "$EC_LOG" +} + +@test "shutdown supersedes teardown when both sentinels exist" { + : > "$TEARDOWN_SENTINEL" + : > "$SHUTDOWN_SENTINEL" + run run_hook + [ "$status" -eq 0 ] + grep -q "cj/ai-term-shutdown-countdown" "$EC_LOG" + ! grep -q "cj/ai-term-quit" "$EC_LOG" + [ ! -f "$TEARDOWN_SENTINEL" ] + [ ! -f "$SHUTDOWN_SENTINEL" ] +} + +@test "emacsclient absent: clears the sentinel and exits 0 (graceful)" { + : > "$TEARDOWN_SENTINEL" + status=0 + output="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="/usr/bin:/bin" bash "$SCRIPT")" || status=$? + [ "$status" -eq 0 ] + [ ! -f "$TEARDOWN_SENTINEL" ] +} + +@test "falls back to PWD basename when cwd is absent from JSON" { + # No cwd key: hook uses $PWD. Run from CWD so basename resolves to PROJ. + : > "$TEARDOWN_SENTINEL" + run env "PATH=$BIN:$PATH" bash -c "cd '$CWD' && printf '{}' | bash '$SCRIPT'" + [ "$status" -eq 0 ] + grep -q "cj/ai-term-quit \"$PROJ\"" "$EC_LOG" +} + +@test "emits no stderr noise on a normal stop" { + err="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \ + | PATH="$BIN:$PATH" bash "$SCRIPT" 2>&1 >/dev/null)" + [ -z "$err" ] +} diff --git a/scripts/tests/install-lang.bats b/scripts/tests/install-lang.bats index ecfbe01..8518852 100644 --- a/scripts/tests/install-lang.bats +++ b/scripts/tests/install-lang.bats @@ -79,6 +79,53 @@ teardown() { grep -qxF "coverage/" "$PROJECT/.gitignore" } +@test "install-lang python: seeds the language-neutral default CLAUDE.md when the bundle ships none" { + run bash "$INSTALL_LANG" python "$PROJECT" + + [ "$status" -eq 0 ] + [ -f "$PROJECT/CLAUDE.md" ] + # The default names no language, so it can't mislabel a python (or bash, or + # multi-bundle) project the way inheriting elisp's "Elisp project" header did. + ! grep -qi "Python project" "$PROJECT/CLAUDE.md" + ! grep -qi "Elisp project" "$PROJECT/CLAUDE.md" + grep -qF "names no language" "$PROJECT/CLAUDE.md" + [[ "$output" == *"language-neutral default"* ]] +} + +@test "install-lang elisp: seeds the bundle's own CLAUDE.md, not the default" { + run bash "$INSTALL_LANG" elisp "$PROJECT" + + [ "$status" -eq 0 ] + grep -qF "Elisp project." "$PROJECT/CLAUDE.md" + [[ "$output" == *"CLAUDE.md installed (elisp)"* ]] +} + +@test "install-lang python: does not overwrite an existing CLAUDE.md without FORCE" { + echo "MY OWN CLAUDE" > "$PROJECT/CLAUDE.md" + run bash "$INSTALL_LANG" python "$PROJECT" + + [ "$status" -eq 0 ] + grep -qxF "MY OWN CLAUDE" "$PROJECT/CLAUDE.md" +} + +@test "install-lang bash: full bundle lands (rules, hook, settings, githook, CLAUDE.md)" { + run bash "$INSTALL_LANG" bash "$PROJECT" + + [ "$status" -eq 0 ] + # Language + testing rules — the bundle's sync fingerprint + [ -f "$PROJECT/.claude/rules/bash.md" ] + [ -f "$PROJECT/.claude/rules/bash-testing.md" ] + # PostToolUse validate hook, executable and wired into settings + [ -x "$PROJECT/.claude/hooks/validate-bash.sh" ] + grep -qF "validate-bash.sh" "$PROJECT/.claude/settings.json" + # Pre-commit githook + [ -x "$PROJECT/githooks/pre-commit" ] + # The bundle ships its own CLAUDE.md, so it wins over the neutral default + grep -qF "Bash/shell project" "$PROJECT/CLAUDE.md" + # Gitignore footprint + grep -qxF ".claude/" "$PROJECT/.gitignore" +} + @test "install-lang go: full bundle lands (rules, hook, settings, githook, CLAUDE.md, coverage)" { run bash "$INSTALL_LANG" go "$PROJECT" |
