diff options
Diffstat (limited to 'scripts/tests/install-lang-collision.bats')
| -rw-r--r-- | scripts/tests/install-lang-collision.bats | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/scripts/tests/install-lang-collision.bats b/scripts/tests/install-lang-collision.bats new file mode 100644 index 0000000..e03e136 --- /dev/null +++ b/scripts/tests/install-lang-collision.bats @@ -0,0 +1,143 @@ +#!/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 all 5 bundles — cp -rT, silently overwritten +# githooks/pre-commit all 5 bundles — cp -rT, silently overwritten +# coverage-makefile.txt 4 bundles — [skip]ped, fragment dropped +# +# The first two read "elisp, bash, go" until 2026-07-23, when python and +# typescript gained the components they had been missing. The consequence is +# that no two shipping bundles compose any more; see the polyglot task. +# +# 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" { + # Named for the pre-2026-07-23 reason: python and typescript shipped no + # settings.json or githooks, so the coverage fragment was their only overlap + # and the second one's used to be silently dropped. They now overlap on all + # three, so this exercises the multi-file refusal path — the coverage + # fragment must still be named among them. + 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" { + # The guard must stay out of the way when nothing overlaps. 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. + # + # This used to be tested with bash + python, which composed because python + # shipped no settings.json and no githooks. That was the incomplete-bundle + # bug (fixed 2026-07-23), not a design property — so no pair of *shipping* + # bundles is non-colliding any more, and the case needs a synthetic bundle. + # See the polyglot task in todo.org: whether every pair now colliding is + # acceptable is an open question, but the guard's own no-false-refusal + # behavior is not, and stays pinned here. + fake="${BATS_TEST_DIRNAME}/../../languages/zz-test-rulesonly" + mkdir -p "$fake/claude/rules" + printf '# rule\n' > "$fake/claude/rules/zz-testing.md" + + bash "$INSTALL" bash "$PROJ" + run bash "$INSTALL" zz-test-rulesonly "$PROJ" + rm -rf "$fake" + + [ "$status" -eq 0 ] || { echo "guard falsely refused a non-colliding pair: $output"; 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/zz-testing.md" ] +} + +@test "install-lang: completing python made it collide with bash (documents the tradeoff)" { + # Pins the consequence of the 2026-07-23 bundle completion so it can't drift + # back unnoticed: python now ships settings.json + githooks/pre-commit, so + # bash + python is a genuine overwrite conflict and the guard refuses it. + # Whether that's the right trade is Craig's open call; that it IS the current + # behavior is what this test records. + bash "$INSTALL" bash "$PROJ" + run bash "$INSTALL" python "$PROJ" + [ "$status" -ne 0 ] + [[ "$output" == *"collision"* ]] +} + +# ---- 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; } +} |
