aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/install-lang-collision.bats
blob: e03e136d2c9331525e66f814b8dce978db8cfeeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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; }
}