#!/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" <