#!/usr/bin/env bats # Tests for install-lang's cross-bundle collision guard. # # Several bundles ship files at the same path. gitignore-add.txt merges # (appended, deduped) and CLAUDE.md is seed-only, so both compose across # bundles. Three do not: # # claude/settings.json elisp, bash, go — cp -rT, silently overwritten # githooks/pre-commit elisp, bash, go — cp -rT, silently overwritten # coverage-makefile.txt 4 bundles — [skip]ped, fragment dropped # # Installing a second bundle used to replace the first's settings.json and # pre-commit while printing [ok], so a project could lose its paren check or # secret scan and read the output as success. The guard refuses instead, naming # what would be replaced. FORCE=1 still overrides. INSTALL="${BATS_TEST_DIRNAME}/../install-lang.sh" setup() { PROJ="$(mktemp -d)" git init -q "$PROJ" } teardown() { [ -n "${PROJ:-}" ] && rm -rf "$PROJ" } # ---- Normal: single-bundle installs are unaffected ---- @test "install-lang: a fresh single-bundle install succeeds" { run bash "$INSTALL" elisp "$PROJ" [ "$status" -eq 0 ] [ -f "$PROJ/.claude/settings.json" ] grep -q 'validate-el.sh' "$PROJ/.claude/settings.json" } @test "install-lang: reinstalling the SAME bundle is idempotent, not a collision" { bash "$INSTALL" elisp "$PROJ" run bash "$INSTALL" elisp "$PROJ" [ "$status" -eq 0 ] [[ "$output" != *"collision"* ]] grep -q 'validate-el.sh' "$PROJ/.claude/settings.json" } # ---- The guard: a second, different bundle must not silently replace ---- @test "install-lang: a second bundle sharing settings.json and githooks is refused" { bash "$INSTALL" elisp "$PROJ" run bash "$INSTALL" bash "$PROJ" [ "$status" -ne 0 ] || { echo "second bundle installed without refusal"; return 1; } [[ "$output" == *"elisp"* ]] || { echo "refusal does not name the existing bundle"; return 1; } } @test "install-lang: the refusal names each file that would be replaced" { bash "$INSTALL" elisp "$PROJ" run bash "$INSTALL" bash "$PROJ" [[ "$output" == *"settings.json"* ]] || { echo "refusal omits settings.json"; return 1; } [[ "$output" == *"pre-commit"* ]] || { echo "refusal omits githooks/pre-commit"; return 1; } } @test "install-lang: a refused install leaves the first bundle intact" { bash "$INSTALL" elisp "$PROJ" bash "$INSTALL" bash "$PROJ" || true grep -q 'validate-el.sh' "$PROJ/.claude/settings.json" \ || { echo "elisp settings.json was replaced despite refusal"; return 1; } grep -q 'check-parens' "$PROJ/githooks/pre-commit" \ || { echo "elisp pre-commit was replaced despite refusal"; return 1; } } @test "install-lang: bundles colliding only on coverage-makefile.txt are refused too" { # python and typescript ship no settings.json or githooks, but both ship a # coverage fragment. The second one's used to be silently dropped. bash "$INSTALL" python "$PROJ" run bash "$INSTALL" typescript "$PROJ" [ "$status" -ne 0 ] || { echo "typescript installed over python's coverage fragment"; return 1; } [[ "$output" == *"coverage-makefile.txt"* ]] } @test "install-lang: two bundles that share no overwritten file install together" { # bash ships settings.json + githooks and no coverage fragment; python ships # only the coverage fragment. Nothing overlaps, so the guard must stay out of # the way. This is the path where a false refusal would be easiest to # introduce: the bundle IS detected, and only the empty file-list stops it. bash "$INSTALL" bash "$PROJ" run bash "$INSTALL" python "$PROJ" [ "$status" -eq 0 ] || { echo "guard falsely refused a non-colliding pair: $output"; return 1; } [ -f "$PROJ/coverage-makefile.txt" ] || { echo "python's coverage fragment did not land"; return 1; } # bash's config survives untouched. grep -q 'validate-bash.sh' "$PROJ/.claude/settings.json" [ -f "$PROJ/.claude/rules/bash.md" ] && [ -f "$PROJ/.claude/rules/python-testing.md" ] } # ---- The escape hatch ---- @test "install-lang: FORCE=1 overrides the collision guard" { bash "$INSTALL" elisp "$PROJ" run bash "$INSTALL" bash "$PROJ" 1 [ "$status" -eq 0 ] || { echo "FORCE=1 did not override: $output"; return 1; } grep -q 'validate-bash.sh' "$PROJ/.claude/settings.json" } @test "install-lang: the refusal points at FORCE=1 and warns it re-seeds CLAUDE.md" { bash "$INSTALL" elisp "$PROJ" run bash "$INSTALL" bash "$PROJ" # Assert the refusal fired first: the pre-existing "[skip] CLAUDE.md already # exists (use FORCE=1 to overwrite)" line mentions both strings on its own, so # without this the test passes against an unguarded install. [ "$status" -ne 0 ] || { echo "no refusal fired"; return 1; } refusal="$(printf '%s\n' "$output" | grep -v '^ \[skip\]')" [[ "$refusal" == *"FORCE=1"* ]] || { echo "refusal does not name the override"; return 1; } [[ "$refusal" == *"CLAUDE.md"* ]] || { echo "refusal does not warn about the CLAUDE.md re-seed"; return 1; } }