aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/tests')
-rw-r--r--scripts/tests/audit.bats140
-rw-r--r--scripts/tests/install-ai.bats103
2 files changed, 243 insertions, 0 deletions
diff --git a/scripts/tests/audit.bats b/scripts/tests/audit.bats
new file mode 100644
index 0000000..3df69c9
--- /dev/null
+++ b/scripts/tests/audit.bats
@@ -0,0 +1,140 @@
+#!/usr/bin/env bats
+#
+# Tests for scripts/audit.sh — cross-project .ai/ drift detector.
+#
+# Strategy: redirect HOME to a temp dir per test, scaffold synthetic
+# project trees under HOME/code/, run the real audit.sh against the
+# synthetic state. The canonical source stays the real one (audit.sh
+# resolves it relative to its own location, not HOME).
+
+REAL_REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+AUDIT="$REAL_REPO/scripts/audit.sh"
+CANONICAL="$REAL_REPO/claude-templates/.ai"
+
+setup() {
+ TEST_HOME="$(mktemp -d -t audit-bats.XXXXXX)"
+ HOME_BAK="$HOME"
+ export HOME="$TEST_HOME"
+ mkdir -p "$TEST_HOME/code" "$TEST_HOME/projects" "$TEST_HOME/.emacs.d"
+}
+
+teardown() {
+ export HOME="$HOME_BAK"
+ rm -rf "$TEST_HOME"
+}
+
+# Mirror canonical .ai/ content into a synthetic project, matching how
+# the startup-rsync would leave a clean .ai/.
+scaffold_synced_ai() {
+ local proj_dir="$1"
+ mkdir -p "$proj_dir/.ai"
+ rsync -a "$CANONICAL/protocols.org" "$proj_dir/.ai/protocols.org"
+ rsync -a --delete "$CANONICAL/workflows/" "$proj_dir/.ai/workflows/"
+ rsync -a --delete "$CANONICAL/scripts/" "$proj_dir/.ai/scripts/"
+}
+
+# Make a project a tracked git checkout with .ai/ committed clean.
+# Subsequent edits to .ai/ then count as uncommitted (dirty) per
+# audit's git status check.
+git_init_with_ai_tracked() {
+ local proj_dir="$1"
+ (cd "$proj_dir" \
+ && git init -q \
+ && git add -A \
+ && git -c user.email=test@test -c user.name=test commit -q -m initial)
+}
+
+@test "audit: clean projects report ok with exit 0" {
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+ scaffold_synced_ai "$TEST_HOME/code/beta"
+
+ run bash "$AUDIT" --no-doctor
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"ok ~/code/alpha"* ]]
+ [[ "$output" == *"ok ~/code/beta"* ]]
+ [[ "$output" == *"Summary: 2 ok, 0 drift, 0 skipped, 0 failed"* ]]
+}
+
+@test "audit: drift detection reports drift and exits 1" {
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+ echo "# drift marker" >> "$TEST_HOME/code/alpha/.ai/protocols.org"
+
+ run bash "$AUDIT" --no-doctor
+
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"drift ~/code/alpha"* ]]
+ [[ "$output" == *"1 drift"* ]]
+}
+
+@test "audit --apply: drifted untracked project converges" {
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+ echo "# drift marker" >> "$TEST_HOME/code/alpha/.ai/protocols.org"
+
+ run bash "$AUDIT" --apply --no-doctor
+
+ # Applied counts as non-ok in the audit summary, so exit 1.
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"applied ~/code/alpha"* ]]
+
+ # Re-run confirms convergence.
+ run bash "$AUDIT" --no-doctor
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"ok ~/code/alpha"* ]]
+}
+
+@test "audit: tracked project with dirty .ai/ is skipped" {
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+ git_init_with_ai_tracked "$TEST_HOME/code/alpha"
+ echo "# uncommitted" >> "$TEST_HOME/code/alpha/.ai/protocols.org"
+
+ run bash "$AUDIT" --apply --no-doctor
+
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"skipped ~/code/alpha"* ]]
+ [[ "$output" == *"use --force"* ]]
+
+ # The dirty edit survived (skip is a safety guard).
+ grep -q "# uncommitted" "$TEST_HOME/code/alpha/.ai/protocols.org"
+}
+
+@test "audit --apply --force: tracked dirty .ai/ gets clobbered" {
+ # Edge case 1 from todo.org:1766. Verifies the override path.
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+ git_init_with_ai_tracked "$TEST_HOME/code/alpha"
+ echo "# uncommitted" >> "$TEST_HOME/code/alpha/.ai/protocols.org"
+
+ run bash "$AUDIT" --apply --force --no-doctor
+
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"applied ~/code/alpha"* ]]
+
+ # The uncommitted edit is gone — force clobbered it.
+ ! grep -q "# uncommitted" "$TEST_HOME/code/alpha/.ai/protocols.org"
+}
+
+@test "audit: loop continues past .ai/-missing failure" {
+ # Edge case 2 from todo.org:1766. The defensive [ ! -d "$proj/.ai" ]
+ # branch fires when a discovered .ai/ disappears between find and
+ # the loop iteration. That race can't be timed reliably, so stub
+ # find to inject a fabricated path; the for-loop hits the FAIL
+ # branch on the ghost and must continue to the real project.
+ scaffold_synced_ai "$TEST_HOME/code/alpha"
+
+ stub_bin="$(mktemp -d)"
+ cat > "$stub_bin/find" <<EOF
+#!/bin/bash
+/usr/bin/find "\$@"
+echo "$TEST_HOME/code/ghost/.ai"
+EOF
+ chmod +x "$stub_bin/find"
+
+ PATH="$stub_bin:$PATH" run bash "$AUDIT" --no-doctor
+ rm -rf "$stub_bin"
+
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"FAIL ~/code/ghost"* ]]
+ [[ "$output" == *".ai/ missing"* ]]
+ # Loop continued past the failure to the real project.
+ [[ "$output" == *"ok ~/code/alpha"* ]]
+}
diff --git a/scripts/tests/install-ai.bats b/scripts/tests/install-ai.bats
new file mode 100644
index 0000000..d67a9c6
--- /dev/null
+++ b/scripts/tests/install-ai.bats
@@ -0,0 +1,103 @@
+#!/usr/bin/env bats
+#
+# Tests for scripts/install-ai.sh — bootstrap .ai/ in a fresh project.
+#
+# Strategy: redirect HOME to a temp dir, scaffold fresh project trees
+# under HOME/code/, run install-ai.sh against them. Canonical source
+# stays the real one (install-ai.sh resolves it relative to its own
+# location). For the fzf-pick form, stub fzf to take the first line.
+
+REAL_REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+INSTALL_AI="$REAL_REPO/scripts/install-ai.sh"
+
+setup() {
+ TEST_HOME="$(mktemp -d -t install-ai-bats.XXXXXX)"
+ HOME_BAK="$HOME"
+ export HOME="$TEST_HOME"
+ mkdir -p "$TEST_HOME/code" "$TEST_HOME/projects"
+}
+
+teardown() {
+ export HOME="$HOME_BAK"
+ rm -rf "$TEST_HOME"
+}
+
+@test "install-ai: happy path with explicit PROJECT + --gitignore" {
+ mkdir -p "$TEST_HOME/code/fresh"
+ (cd "$TEST_HOME/code/fresh" && git init -q)
+
+ run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/fresh"
+
+ [ "$status" -eq 0 ]
+ [ -d "$TEST_HOME/code/fresh/.ai/workflows" ]
+ [ -d "$TEST_HOME/code/fresh/.ai/scripts" ]
+ [ -d "$TEST_HOME/code/fresh/.ai/sessions" ]
+ [ -d "$TEST_HOME/code/fresh/.ai/references" ]
+ [ -d "$TEST_HOME/code/fresh/.ai/retrospectives" ]
+ [ -f "$TEST_HOME/code/fresh/.ai/protocols.org" ]
+ [ -f "$TEST_HOME/code/fresh/.ai/notes.org" ]
+ grep -qFx ".ai/" "$TEST_HOME/code/fresh/.gitignore"
+}
+
+@test "install-ai --track: lands .gitkeep stubs in empty dirs" {
+ mkdir -p "$TEST_HOME/code/tracked"
+ (cd "$TEST_HOME/code/tracked" && git init -q)
+
+ run bash "$INSTALL_AI" --track "$TEST_HOME/code/tracked"
+
+ [ "$status" -eq 0 ]
+ [ -f "$TEST_HOME/code/tracked/.ai/sessions/.gitkeep" ]
+ [ -f "$TEST_HOME/code/tracked/.ai/references/.gitkeep" ]
+ [ -f "$TEST_HOME/code/tracked/.ai/retrospectives/.gitkeep" ]
+}
+
+@test "install-ai: refuses on existing .ai/" {
+ mkdir -p "$TEST_HOME/code/already/.ai"
+ echo "marker" > "$TEST_HOME/code/already/.ai/marker.txt"
+
+ run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/already"
+
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"already exists"* ]]
+ # Existing content untouched.
+ [ -f "$TEST_HOME/code/already/.ai/marker.txt" ]
+}
+
+@test "install-ai: notes.org placeholders get substituted" {
+ mkdir -p "$TEST_HOME/code/named-proj"
+ (cd "$TEST_HOME/code/named-proj" && git init -q)
+
+ run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/named-proj"
+
+ [ "$status" -eq 0 ]
+ # Project name landed.
+ grep -q "named-proj" "$TEST_HOME/code/named-proj/.ai/notes.org"
+ # No raw placeholders left.
+ ! grep -q "\[Project Name\]" "$TEST_HOME/code/named-proj/.ai/notes.org"
+ ! grep -q "\[Date\]" "$TEST_HOME/code/named-proj/.ai/notes.org"
+}
+
+@test "install-ai: fzf-pick form selects via stubbed fzf" {
+ # Edge case 3 from todo.org:1766. The fzf-pick form is interactive;
+ # stubbing fzf to take the first stdin line lets us exercise the
+ # discovery+selection path without an interactive terminal.
+ mkdir -p "$TEST_HOME/code/pickme" "$TEST_HOME/code/skipme"
+ (cd "$TEST_HOME/code/pickme" && git init -q)
+ (cd "$TEST_HOME/code/skipme" && git init -q)
+
+ stub_bin="$(mktemp -d)"
+ cat > "$stub_bin/fzf" <<'EOF'
+#!/bin/bash
+# Pass through stdin's first line as the "selection".
+head -n 1
+EOF
+ chmod +x "$stub_bin/fzf"
+
+ PATH="$stub_bin:$PATH" run bash "$INSTALL_AI" --gitignore
+ rm -rf "$stub_bin"
+
+ [ "$status" -eq 0 ]
+ # Sort order puts pickme first; fzf-stub returns the first line.
+ [ -d "$TEST_HOME/code/pickme/.ai" ]
+ [ ! -d "$TEST_HOME/code/skipme/.ai" ]
+}