aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 21:00:11 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 21:00:11 -0400
commit603abc4cb3129be8bd23c89aa69f4f5522d1e5a3 (patch)
tree356c6fc1343e4564d63b91b269aa8f65c1a40236 /.ai/scripts/tests
parentd961c783d18c6178751b338ef1d8dd6a72db9f20 (diff)
downloadrulesets-603abc4cb3129be8bd23c89aa69f4f5522d1e5a3.tar.gz
rulesets-603abc4cb3129be8bd23c89aa69f4f5522d1e5a3.zip
feat(inbox-zero): guard roam-inbox writes against live org-capture
Editing the roam inbox on disk while Emacs has an indirect org-capture buffer cloned from it reverts the base buffer under the capture: the capture can't finalize with C-c C-c, and a freshly-typed item can be lost. inbox-zero Phase D edits that file, which Craig captures into constantly, so the collision recurs every session. I added a capture-guard helper that asks the running daemon whether any CAPTURE buffer's base buffer visits a given file (file-equal-p, so symlinks and path spelling don't matter), exiting non-zero with the names when so. No reachable Emacs or no capture means exit 0, so it never blocks a write that was safe. Phase D calls it before the pull, not only before the remove, because the ff-only pull also rewrites the file on disk and would wedge a capture the same way. On a collision an on-demand run stops and asks Craig to finalize or abort. The wrap-up sub-step skips the roam reconcile without blocking the wrap, since the items are already filed and the next run reclaims them. emacs.md gains the inverse of the reload rule: don't yank a file out from under the daemon's live buffers.
Diffstat (limited to '.ai/scripts/tests')
-rw-r--r--.ai/scripts/tests/capture-guard.bats103
1 files changed, 103 insertions, 0 deletions
diff --git a/.ai/scripts/tests/capture-guard.bats b/.ai/scripts/tests/capture-guard.bats
new file mode 100644
index 0000000..12ecb83
--- /dev/null
+++ b/.ai/scripts/tests/capture-guard.bats
@@ -0,0 +1,103 @@
+#!/usr/bin/env bats
+#
+# Tests for claude-templates/.ai/scripts/capture-guard — detects live
+# org-capture buffers visiting a target file before a workflow edits that
+# file on disk (the roam inbox, in inbox-zero Phase D). Editing the file
+# underneath an indirect org-capture buffer wedges the capture (see emacs.md).
+#
+# Contract under test:
+# capture-guard [TARGET_FILE] (default TARGET_FILE = ~/org/roam/inbox.org)
+# exit 0 → safe to edit: emacsclient absent, daemon unreachable, or no
+# capture buffer visits TARGET_FILE.
+# exit 1 → a live capture buffer visits TARGET_FILE; its name(s) printed.
+#
+# Strategy: the emacsclient boundary is mocked with a PATH stub. The stub
+# answers the reachability probe (`-e t`) per STUB_REACHABLE and returns a
+# canned, real-emacsclient-shaped result (quoted string) for the buffer query
+# per STUB_BUFS. The script's own quote-stripping and exit logic is the code
+# under test; the file-equal-p precision is real-Emacs behavior we trust.
+
+SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/capture-guard"
+BASH_BIN="$(command -v bash)"
+
+setup() {
+ TEST_DIR="$(mktemp -d -t capture-guard-bats.XXXXXX)"
+ STUB_DIR="$TEST_DIR/bin"
+ mkdir -p "$STUB_DIR"
+
+ cat > "$STUB_DIR/emacsclient" <<'STUB'
+#!/usr/bin/env bash
+# Mock emacsclient. `-e t` is the reachability probe; anything else is the
+# buffer query, answered with the real-emacsclient-shaped quoted string.
+expr="$2"
+if [ "$expr" = "t" ]; then
+ [ "${STUB_REACHABLE:-1}" = "1" ] && { echo t; exit 0; }
+ exit 1
+fi
+printf '%s\n' "${STUB_BUFS:-\"\"}"
+exit 0
+STUB
+ chmod +x "$STUB_DIR/emacsclient"
+
+ EMPTY_DIR="$TEST_DIR/empty"
+ mkdir -p "$EMPTY_DIR"
+}
+
+teardown() {
+ rm -rf "$TEST_DIR"
+}
+
+# ---- Safe-to-edit (exit 0) cases ------------------------------------
+
+@test "capture-guard: emacsclient absent is safe (exit 0, no output)" {
+ run env PATH="$EMPTY_DIR" "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "capture-guard: daemon unreachable is safe (exit 0)" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=0 "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "capture-guard: reachable with no capture buffers is safe (exit 0)" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='""' "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+# ---- Blocked (exit 1) cases -----------------------------------------
+
+@test "capture-guard: one live capture buffer blocks (exit 1, name printed)" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='"CAPTURE-inbox.org"' \
+ "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"CAPTURE-inbox.org"* ]]
+}
+
+@test "capture-guard: multiple live capture buffers all reported (exit 1)" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 \
+ STUB_BUFS='"CAPTURE-inbox.org,CAPTURE-2-inbox.org"' \
+ "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"CAPTURE-inbox.org"* ]]
+ [[ "$output" == *"CAPTURE-2-inbox.org"* ]]
+}
+
+@test "capture-guard: blocked output does not contain stray surrounding quotes" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='"CAPTURE-inbox.org"' \
+ "$BASH_BIN" "$SCRIPT"
+ [ "$status" -eq 1 ]
+ [[ "$output" != \"* ]]
+ [[ "$output" != *\" ]]
+}
+
+# ---- Argument handling ----------------------------------------------
+
+@test "capture-guard: accepts an explicit target-file argument" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='""' \
+ "$BASH_BIN" "$SCRIPT" "$TEST_DIR/some-other-inbox.org"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}