aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/tests/ai-launcher-characterization.bats41
-rw-r--r--scripts/tests/ai-wrap-teardown-hook.bats66
-rwxr-xr-xscripts/tests/git-worktree-gate.bats146
-rw-r--r--scripts/tests/install-hooks-link.bats15
4 files changed, 264 insertions, 4 deletions
diff --git a/scripts/tests/ai-launcher-characterization.bats b/scripts/tests/ai-launcher-characterization.bats
index f53855f..5b93ff3 100644
--- a/scripts/tests/ai-launcher-characterization.bats
+++ b/scripts/tests/ai-launcher-characterization.bats
@@ -120,6 +120,14 @@ _mk_repo_upstream() {
[ "$output" = " (no upstream dirty)" ]
}
+@test "git_status_indicator: inbox-only delivery is visible but not called dirty" {
+ _mk_repo "$WORK/r"
+ mkdir -p "$WORK/r/inbox"
+ touch "$WORK/r/inbox/from-home.org"
+ run git_status_indicator "$WORK/r"
+ [ "$output" = " (no upstream inbox)" ]
+}
+
@test "git_status_indicator: in-sync tracked repo reads (✓)" {
_mk_repo_upstream "$WORK/r"
run git_status_indicator "$WORK/r"
@@ -142,6 +150,39 @@ _mk_repo_upstream() {
[ "$output" = " (↓1)" ]
}
+@test "auto_pull_if_clean fast-forwards with an untracked inbox delivery" {
+ _mk_repo_upstream "$WORK/r"
+ git clone -q "$WORK/r.remote" "$WORK/writer"
+ git -C "$WORK/writer" config user.email t@example.com
+ git -C "$WORK/writer" config user.name tester
+ git -C "$WORK/writer" commit -q --allow-empty -m remote
+ git -C "$WORK/writer" push -q
+ git -C "$WORK/r" fetch -q
+ mkdir -p "$WORK/r/inbox"
+ printf 'handoff\n' >"$WORK/r/inbox/from-home.org"
+
+ run auto_pull_if_clean "$WORK/r"
+ [ "$status" -eq 0 ]
+ [ "$(git -C "$WORK/r" rev-parse HEAD)" = "$(git -C "$WORK/r" rev-parse '@{u}')" ]
+ [ -f "$WORK/r/inbox/from-home.org" ]
+}
+
+@test "auto_pull_if_clean refuses a non-inbox untracked file" {
+ _mk_repo_upstream "$WORK/r"
+ git clone -q "$WORK/r.remote" "$WORK/writer"
+ git -C "$WORK/writer" config user.email t@example.com
+ git -C "$WORK/writer" config user.name tester
+ git -C "$WORK/writer" commit -q --allow-empty -m remote
+ git -C "$WORK/writer" push -q
+ git -C "$WORK/r" fetch -q
+ printf 'scratch\n' >"$WORK/r/scratch"
+ before="$(git -C "$WORK/r" rev-parse HEAD)"
+
+ run auto_pull_if_clean "$WORK/r"
+ [ "$status" -eq 0 ]
+ [ "$(git -C "$WORK/r" rev-parse HEAD)" = "$before" ]
+}
+
# --- annotate_candidates() ----------------------------------------------------
@test "annotate_candidates: appends each candidate's status suffix" {
diff --git a/scripts/tests/ai-wrap-teardown-hook.bats b/scripts/tests/ai-wrap-teardown-hook.bats
index 05c49f1..6e33cca 100644
--- a/scripts/tests/ai-wrap-teardown-hook.bats
+++ b/scripts/tests/ai-wrap-teardown-hook.bats
@@ -7,10 +7,18 @@
setup() {
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
SCRIPT="$REPO_ROOT/hooks/ai-wrap-teardown.sh"
+ GATE="$REPO_ROOT/claude-templates/bin/git-worktree-gate"
TMPDIR_T="$(mktemp -d)"
PROJ="proj-$$-$BATS_TEST_NUMBER" # unique so /tmp sentinels don't collide
CWD="$TMPDIR_T/$PROJ"
mkdir -p "$CWD"
+ git init -q "$CWD"
+ git -C "$CWD" config user.email test@example.com
+ git -C "$CWD" config user.name tester
+ printf 'base\n' >"$CWD/tracked"
+ git -C "$CWD" add tracked
+ git -C "$CWD" commit -qm init
+ bash "$GATE" certify "$CWD"
TEARDOWN_SENTINEL="/tmp/ai-wrap-teardown-${PROJ}"
SHUTDOWN_SENTINEL="/tmp/ai-wrap-shutdown-${PROJ}"
@@ -35,7 +43,7 @@ teardown() {
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"
+ | GIT_WORKTREE_GATE="$GATE" PATH="$BIN:$PATH" bash "$SCRIPT"
}
@test "no sentinel: silent no-op, emacsclient never called" {
@@ -81,7 +89,7 @@ run_hook() {
: > "$TEARDOWN_SENTINEL"
status=0
output="$(printf '{"cwd":"%s","hook_event_name":"Stop"}' "$CWD" \
- | PATH="/usr/bin:/bin" bash "$SCRIPT")" || status=$?
+ | GIT_WORKTREE_GATE="$GATE" PATH="/usr/bin:/bin" bash "$SCRIPT")" || status=$?
[ "$status" -eq 0 ]
[ ! -f "$TEARDOWN_SENTINEL" ]
}
@@ -89,13 +97,63 @@ run_hook() {
@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'"
+ run env "GIT_WORKTREE_GATE=$GATE" "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)"
+ | GIT_WORKTREE_GATE="$GATE" PATH="$BIN:$PATH" bash "$SCRIPT" 2>&1 >/dev/null)"
[ -z "$err" ]
}
+
+@test "dirty worktree blocks teardown, preserves sentinel, and names the path" {
+ printf 'late\n' >>"$CWD/tracked"
+ : >"$TEARDOWN_SENTINEL"
+ run run_hook
+ [ "$status" -eq 0 ]
+ echo "$output" | jq -e '.decision == "block"'
+ echo "$output" | jq -e '.reason | test("tracked")'
+ [ -f "$TEARDOWN_SENTINEL" ]
+ [ ! -f "$EC_LOG" ]
+}
+
+@test "changed HEAD after certification blocks teardown" {
+ git -C "$CWD" commit -q --allow-empty -m later
+ : >"$TEARDOWN_SENTINEL"
+ run run_hook
+ echo "$output" | jq -e '.reason | test("HEAD changed")'
+ [ -f "$TEARDOWN_SENTINEL" ]
+ [ ! -f "$EC_LOG" ]
+}
+
+@test "missing certificate blocks teardown" {
+ rm -f "$(git -C "$CWD" rev-parse --absolute-git-dir)/ai-wrap-clean"
+ : >"$TEARDOWN_SENTINEL"
+ run run_hook
+ echo "$output" | jq -e '.reason | test("no clean-tree certificate")'
+ [ -f "$TEARDOWN_SENTINEL" ]
+}
+
+@test "Codex payload receives continue false and a stop reason" {
+ printf 'late\n' >>"$CWD/tracked"
+ : >"$TEARDOWN_SENTINEL"
+ run bash -c \
+ "printf '{\"cwd\":\"$CWD\",\"hook_event_name\":\"Stop\",\"model\":\"gpt-test\"}' \
+ | GIT_WORKTREE_GATE='$GATE' PATH='$BIN:$PATH' bash '$SCRIPT'"
+ [ "$status" -eq 0 ]
+ echo "$output" | jq -e '.continue == false'
+ echo "$output" | jq -e '.stopReason | test("tracked")'
+ [ -f "$TEARDOWN_SENTINEL" ]
+}
+
+@test "successful teardown consumes the clean-tree certificate" {
+ : >"$TEARDOWN_SENTINEL"
+ cert="$(git -C "$CWD" rev-parse --absolute-git-dir)/ai-wrap-clean"
+ [ -f "$cert" ]
+ run run_hook
+ [ "$status" -eq 0 ]
+ [ ! -f "$cert" ]
+}
diff --git a/scripts/tests/git-worktree-gate.bats b/scripts/tests/git-worktree-gate.bats
new file mode 100755
index 0000000..7d0d8a1
--- /dev/null
+++ b/scripts/tests/git-worktree-gate.bats
@@ -0,0 +1,146 @@
+#!/usr/bin/env bats
+
+setup() {
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+ GATE="$REPO_ROOT/claude-templates/bin/git-worktree-gate"
+ WORK="$(mktemp -d)"
+ REPO="$WORK/repo"
+ git init -q "$REPO"
+ git -C "$REPO" config user.email test@example.com
+ git -C "$REPO" config user.name tester
+ printf 'base\n' >"$REPO/tracked"
+ git -C "$REPO" add tracked
+ git -C "$REPO" commit -qm init
+}
+
+teardown() {
+ rm -rf "$WORK"
+}
+
+@test "strict accepts a completely clean worktree" {
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "strict rejects unstaged, staged, and untracked changes with paths" {
+ printf 'changed\n' >>"$REPO/tracked"
+ printf 'new\n' >"$REPO/staged"
+ git -C "$REPO" add staged
+ printf 'loose\n' >"$REPO/loose"
+
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"wrap blocked"* ]]
+ [[ "$output" == *"tracked"* ]]
+ [[ "$output" == *"staged"* ]]
+ [[ "$output" == *"loose"* ]]
+}
+
+@test "sync-safe permits untracked inbox deliveries and strict still rejects them" {
+ mkdir -p "$REPO/inbox/nested"
+ printf 'handoff\n' >"$REPO/inbox/nested/from-home.org"
+
+ run bash "$GATE" sync-safe "$REPO"
+ [ "$status" -eq 0 ]
+
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"inbox/nested/from-home.org"* ]]
+}
+
+@test "sync-safe rejects tracked changes even inside inbox" {
+ mkdir -p "$REPO/inbox"
+ printf 'tracked\n' >"$REPO/inbox/tracked.org"
+ git -C "$REPO" add inbox/tracked.org
+ git -C "$REPO" commit -qm inbox
+ printf 'changed\n' >>"$REPO/inbox/tracked.org"
+
+ run bash "$GATE" sync-safe "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"inbox/tracked.org"* ]]
+}
+
+@test "sync-safe rejects untracked files outside inbox" {
+ printf 'scratch\n' >"$REPO/scratch"
+ run bash "$GATE" sync-safe "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"scratch"* ]]
+}
+
+@test "ignored files do not block either policy" {
+ printf 'cache/\n' >"$REPO/.gitignore"
+ git -C "$REPO" add .gitignore
+ git -C "$REPO" commit -qm ignore
+ mkdir -p "$REPO/cache"
+ printf 'generated\n' >"$REPO/cache/output"
+
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 0 ]
+ run bash "$GATE" sync-safe "$REPO"
+ [ "$status" -eq 0 ]
+}
+
+@test "reports unusual filenames without losing the entry" {
+ odd=$'line break\nname'
+ printf 'odd\n' >"$REPO/$odd"
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"line"* ]]
+ [[ "$output" == *"name"* ]]
+}
+
+@test "certify and verify bind a clean worktree to its current HEAD" {
+ run bash "$GATE" certify "$REPO"
+ [ "$status" -eq 0 ]
+ run bash "$GATE" verify "$REPO"
+ [ "$status" -eq 0 ]
+
+ git -C "$REPO" commit -q --allow-empty -m later
+ run bash "$GATE" verify "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"HEAD changed"* ]]
+}
+
+@test "verify rejects changes made after certification" {
+ run bash "$GATE" certify "$REPO"
+ [ "$status" -eq 0 ]
+ printf 'late\n' >>"$REPO/tracked"
+ run bash "$GATE" verify "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"tracked"* ]]
+}
+
+@test "dirty submodule blocks strict and sync-safe" {
+ CHILD="$WORK/child"
+ git init -q "$CHILD"
+ git -C "$CHILD" config user.email test@example.com
+ git -C "$CHILD" config user.name tester
+ printf 'child\n' >"$CHILD/file"
+ git -C "$CHILD" add file
+ git -C "$CHILD" commit -qm init
+ git -C "$REPO" -c protocol.file.allow=always submodule add -q "$CHILD" sub
+ git -C "$REPO" commit -qam submodule
+ printf 'dirty\n' >>"$REPO/sub/file"
+
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"sub"* ]]
+ run bash "$GATE" sync-safe "$REPO"
+ [ "$status" -eq 1 ]
+}
+
+@test "a low-level git status failure blocks instead of looking clean" {
+ printf 'not an index\n' >"$WORK/bad-index"
+ run env GIT_INDEX_FILE="$WORK/bad-index" bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"git status failed"* ]]
+}
+
+@test "an in-progress sequencer operation blocks a clean-looking tree" {
+ gitdir="$(git -C "$REPO" rev-parse --absolute-git-dir)"
+ mkdir -p "$gitdir/sequencer"
+ run bash "$GATE" strict "$REPO"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"sequencer"* ]]
+}
diff --git a/scripts/tests/install-hooks-link.bats b/scripts/tests/install-hooks-link.bats
index 80ac8dd..5368781 100644
--- a/scripts/tests/install-hooks-link.bats
+++ b/scripts/tests/install-hooks-link.bats
@@ -20,6 +20,7 @@ run_install() {
RULES_DIR="$TMPHOME/rules" \
HOOKS_DIR="$TMPHOME/hooks" \
CLAUDE_DIR="$TMPHOME/claude" \
+ CODEX_DIR="$TMPHOME/codex" \
LOCAL_BIN="$TMPHOME/bin"
}
@@ -28,6 +29,20 @@ run_install() {
[ "$status" -eq 0 ]
[ -L "$TMPHOME/hooks/session-clear-resume.sh" ]
[ -L "$TMPHOME/hooks/precompact-priorities.sh" ]
+ [ -L "$TMPHOME/hooks/rulesets-write-boundary.py" ]
+}
+
+@test "install links Codex Stop-hook configuration" {
+ run run_install
+ [ "$status" -eq 0 ]
+ [ -L "$TMPHOME/codex/hooks.json" ]
+ grep -q "ai-wrap-teardown.sh" "$TMPHOME/codex/hooks.json"
+}
+
+@test "install links the shared Git worktree gate beside the launcher" {
+ run run_install
+ [ "$status" -eq 0 ]
+ [ -L "$TMPHOME/bin/git-worktree-gate" ]
}
@test "install does not link opt-in hooks" {