aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/sync-language-bundle.bats
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/tests/sync-language-bundle.bats')
-rw-r--r--scripts/tests/sync-language-bundle.bats153
1 files changed, 153 insertions, 0 deletions
diff --git a/scripts/tests/sync-language-bundle.bats b/scripts/tests/sync-language-bundle.bats
new file mode 100644
index 0000000..9fd1108
--- /dev/null
+++ b/scripts/tests/sync-language-bundle.bats
@@ -0,0 +1,153 @@
+#!/usr/bin/env bats
+#
+# Tests for scripts/sync-language-bundle.sh — per-project language-bundle
+# freshness check for startup.
+#
+# Strategy: scaffold a synthetic project in a temp dir with a clean bundle
+# (mirroring how install-lang.sh leaves .claude/), then perturb files and
+# run the real script against it. Canonical source stays the real one (the
+# script resolves languages/ + claude-rules/ relative to its own location).
+
+REAL_REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+SCRIPT="$REAL_REPO/scripts/sync-language-bundle.sh"
+
+setup() {
+ PROJ="$(mktemp -d -t synclang-bats.XXXXXX)"
+}
+
+teardown() {
+ rm -rf "$PROJ"
+}
+
+# Mirror install-lang.sh: copy the bundle's files into a synthetic project.
+install_bundle() {
+ local lang="$1" proj="$2"
+ mkdir -p "$proj/.claude/rules"
+ cp "$REAL_REPO/claude-rules"/*.md "$proj/.claude/rules/"
+ cp "$REAL_REPO/languages/$lang/claude/rules/"*.md "$proj/.claude/rules/" 2>/dev/null || true
+ if [ -d "$REAL_REPO/languages/$lang/claude/hooks" ]; then
+ mkdir -p "$proj/.claude/hooks"
+ cp -r "$REAL_REPO/languages/$lang/claude/hooks/." "$proj/.claude/hooks/"
+ fi
+ if [ -f "$REAL_REPO/languages/$lang/claude/settings.json" ]; then
+ cp "$REAL_REPO/languages/$lang/claude/settings.json" "$proj/.claude/settings.json"
+ fi
+ if [ -d "$REAL_REPO/languages/$lang/githooks" ]; then
+ mkdir -p "$proj/githooks"
+ cp -r "$REAL_REPO/languages/$lang/githooks/." "$proj/githooks/"
+ fi
+ # CLAUDE.md deliberately not seeded — the sync must treat its absence as
+ # fine (it's seed-only / project-owned, not bundle-tracked).
+}
+
+matches_canonical() { # project-relative-path canonical-abs-path
+ diff -q "$PROJ/$1" "$2" >/dev/null 2>&1
+}
+
+# --- Normal: no bundle / clean ---
+
+@test "sync: project with no bundle is a quiet no-op (exit 0)" {
+ mkdir -p "$PROJ/.claude/rules" # generic rules only, no language fingerprint
+ cp "$REAL_REPO/claude-rules"/commits.md "$PROJ/.claude/rules/"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "sync: clean elisp bundle is a quiet no-op (exit 0)" {
+ install_bundle elisp "$PROJ"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "sync: absent CLAUDE.md is not flagged as drift (seed-only/project-owned)" {
+ install_bundle elisp "$PROJ" # helper never seeds CLAUDE.md
+ [ ! -f "$PROJ/CLAUDE.md" ]
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+ [[ "$output" != *"CLAUDE.md"* ]]
+}
+
+# --- Auto-fix: rules ---
+
+@test "sync: drifted language rule is auto-fixed and restored" {
+ install_bundle elisp "$PROJ"
+ echo ";; junk drift" >> "$PROJ/.claude/rules/elisp.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"fixed"* ]]
+ [[ "$output" == *".claude/rules/elisp.md"* ]]
+ matches_canonical ".claude/rules/elisp.md" "$REAL_REPO/languages/elisp/claude/rules/elisp.md"
+}
+
+@test "sync: drifted generic rule is auto-fixed and restored" {
+ install_bundle elisp "$PROJ"
+ echo "junk" >> "$PROJ/.claude/rules/commits.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *".claude/rules/commits.md"* ]]
+ matches_canonical ".claude/rules/commits.md" "$REAL_REPO/claude-rules/commits.md"
+}
+
+@test "sync: missing rule is re-copied" {
+ install_bundle elisp "$PROJ"
+ rm "$PROJ/.claude/rules/elisp-testing.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -f "$PROJ/.claude/rules/elisp-testing.md" ]
+ matches_canonical ".claude/rules/elisp-testing.md" "$REAL_REPO/languages/elisp/claude/rules/elisp-testing.md"
+}
+
+# --- Auto-fix: hooks ---
+
+@test "sync: drifted hook is auto-fixed and stays executable" {
+ install_bundle elisp "$PROJ"
+ echo "# junk" >> "$PROJ/.claude/hooks/validate-el.sh"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"validate-el.sh"* ]]
+ matches_canonical ".claude/hooks/validate-el.sh" "$REAL_REPO/languages/elisp/claude/hooks/validate-el.sh"
+ [ -x "$PROJ/.claude/hooks/validate-el.sh" ]
+}
+
+# --- Surface-only: settings.json ---
+
+@test "sync: drifted settings.json is surfaced, NOT modified, exit 3" {
+ install_bundle elisp "$PROJ"
+ echo '{"_drift": true}' > "$PROJ/.claude/settings.json"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 3 ]
+ [[ "$output" == *"settings.json"* ]]
+ [[ "$output" == *"not auto-fixed"* ]]
+ # file left untouched — still carries the perturbation
+ grep -q "_drift" "$PROJ/.claude/settings.json"
+}
+
+# --- Other language ---
+
+@test "sync: python bundle detected via python-testing.md and fixed" {
+ install_bundle python "$PROJ"
+ echo "junk" >> "$PROJ/.claude/rules/python-testing.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"python-testing.md"* ]]
+ matches_canonical ".claude/rules/python-testing.md" "$REAL_REPO/languages/python/claude/rules/python-testing.md"
+}
+
+# --- Boundary / Error ---
+
+@test "sync: defaults to \$PWD when no path given" {
+ install_bundle elisp "$PROJ"
+ echo "junk" >> "$PROJ/.claude/rules/elisp.md"
+ run bash -c "cd '$PROJ' && bash '$SCRIPT'"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"elisp.md"* ]]
+}
+
+@test "sync: nonexistent project path errors with exit 1" {
+ run bash "$SCRIPT" "/no/such/path/here"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"does not exist"* ]]
+}