aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/install-lang-collision.bats
blob: 36abb5b423e219b523154b7c88dbdfcfb8bc16f9 (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
#!/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; }
}