diff options
Diffstat (limited to 'scripts/tests')
| -rw-r--r-- | scripts/tests/audit.bats | 140 | ||||
| -rw-r--r-- | scripts/tests/install-ai.bats | 103 |
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" ] +} |
