aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests
diff options
context:
space:
mode:
Diffstat (limited to '.ai/scripts/tests')
-rw-r--r--.ai/scripts/tests/capture-guard.bats130
-rw-r--r--.ai/scripts/tests/test_flashcard_to_anki.py31
2 files changed, 155 insertions, 6 deletions
diff --git a/.ai/scripts/tests/capture-guard.bats b/.ai/scripts/tests/capture-guard.bats
new file mode 100644
index 0000000..31632a4
--- /dev/null
+++ b/.ai/scripts/tests/capture-guard.bats
@@ -0,0 +1,130 @@
+#!/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.org roam mode 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" ]
+}
+
+# ---- --wait poll mode -----------------------------------------------
+
+@test "capture-guard --wait: returns 0 instantly when already safe (no sleep)" {
+ SECONDS=0
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='""' \
+ "$BASH_BIN" "$SCRIPT" --wait
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+ [ "$SECONDS" -lt 2 ] # didn't poll-sleep
+}
+
+@test "capture-guard --wait=1: times out to exit 1 when persistently blocked" {
+ # Stub always reports the buffer, so it never clears — the short budget
+ # forces a timeout. Capped sleep keeps this near 1s.
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='"CAPTURE-inbox.org"' \
+ "$BASH_BIN" "$SCRIPT" --wait=1
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"CAPTURE-inbox.org"* ]]
+}
+
+@test "capture-guard --wait=N accepts a target after the flag" {
+ run env PATH="$STUB_DIR:$PATH" STUB_REACHABLE=1 STUB_BUFS='""' \
+ "$BASH_BIN" "$SCRIPT" --wait=1 "$TEST_DIR/some-other-inbox.org"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
diff --git a/.ai/scripts/tests/test_flashcard_to_anki.py b/.ai/scripts/tests/test_flashcard_to_anki.py
index 058b0cd..87008a8 100644
--- a/.ai/scripts/tests/test_flashcard_to_anki.py
+++ b/.ai/scripts/tests/test_flashcard_to_anki.py
@@ -34,14 +34,33 @@ def test_default_output_path_targets_phone_anki_dir(drill):
assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg"
-def test_default_deck_name_is_raw_basename(drill):
- """Deck name is the input basename with case preserved; #+TITLE is ignored."""
- assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat"
+def test_default_deck_name_uses_org_title(drill):
+ """The #+TITLE drives the Anki deck name, not the filename slug."""
+ org = "#+TITLE: Refutations\n* Section\n** Q? :drill:\na\n"
+ assert drill.default_deck_name(Path("/x/refutation-drill.org"), org) == "Refutations"
-def test_default_deck_name_keeps_hyphens(drill):
- """A hyphenated basename is kept verbatim rather than title-cased."""
- assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"
+def test_default_deck_name_title_is_trimmed(drill):
+ """Surrounding whitespace on the #+TITLE value is stripped."""
+ org = "#+TITLE: DeepSat Flashcards \n"
+ assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "DeepSat Flashcards"
+
+
+def test_default_deck_name_title_match_is_case_insensitive(drill):
+ """A lowercase #+title: keyword is still recognized."""
+ org = "#+title: Health Flashcards\n"
+ assert drill.default_deck_name(Path("/x/health-drill.org"), org) == "Health Flashcards"
+
+
+def test_default_deck_name_falls_back_to_basename_without_title(drill):
+ """No #+TITLE line falls back to the input basename, case preserved."""
+ org = "* Section\n** Q? :drill:\na\n"
+ assert drill.default_deck_name(Path("/x/deepsat.org"), org) == "deepsat"
+
+
+def test_default_deck_name_blank_title_falls_back_to_basename(drill):
+ """An empty #+TITLE value is ignored in favour of the basename."""
+ assert drill.default_deck_name(Path("/x/health-drill.org"), "#+TITLE: \n") == "health-drill"
# --- section_to_tag (pure) ---