diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-23 23:34:18 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-23 23:34:18 -0400 |
| commit | f87f59cc9eb1dd492be5b55870271d67245c1fdd (patch) | |
| tree | 18bab578f4026f9f85878bd0bfc53d147eab470a /scripts | |
| parent | e5aab199cd4c83f357ff5190139ddf2994ac28a3 (diff) | |
| download | rulesets-f87f59cc9eb1dd492be5b55870271d67245c1fdd.tar.gz rulesets-f87f59cc9eb1dd492be5b55870271d67245c1fdd.zip | |
feat(wrap): add session teardown and shutdown to wrap-it-up
A bare "wrap it up" now tears the session down after the valediction: it kills the ai-term buffer and the aiv-<project> tmux session (which takes claude with it) and restores geometry. "wrap it up with summary" or "and summarize" keeps the buffer. "wrap it up and shutdown" gates on this being the only live ai-term session, then powers the machine off through an abort-able Emacs countdown.
Teardown can't run inline because it kills the session claude runs in, so the valediction would never flush. Step 6 instead drops a basename-keyed sentinel after commit+push is verified, and a new Stop hook (ai-wrap-teardown.sh) does the teardown when the response ends, by which point the valediction has rendered. The hook is a no-op on every normal stop because the sentinel only exists after a teardown wrap.
The runtime lives in .emacs.d/modules/ai-term.el (cj/ai-term-quit, cj/ai-term-live-count, cj/ai-term-shutdown-countdown), and the rulesets side calls it via emacsclient. I routed that companion to .emacs.d, so the feature is end-to-end once it lands. The hook has 8 bats tests. The live teardown and shutdown paths are a manual checklist in todo.org.
Built from the proposal. I went with both summary qualifiers, the Emacs-timer countdown, and the live-count gate.
Claude-Session: https://claude.ai/code/session_017PtX1nt1rtYVATuzmzBS4f
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/tests/ai-wrap-teardown-hook.bats | 101 |
1 files changed, 101 insertions, 0 deletions
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" ] +} |
