diff options
Diffstat (limited to 'scripts/tests')
| -rw-r--r-- | scripts/tests/install-lang-collision.bats | 53 | ||||
| -rw-r--r-- | scripts/tests/install-lang-completeness.bats | 93 | ||||
| -rw-r--r-- | scripts/tests/install-lang.bats | 62 |
3 files changed, 192 insertions, 16 deletions
diff --git a/scripts/tests/install-lang-collision.bats b/scripts/tests/install-lang-collision.bats index 36abb5b..e03e136 100644 --- a/scripts/tests/install-lang-collision.bats +++ b/scripts/tests/install-lang-collision.bats @@ -5,10 +5,14 @@ # (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 +# 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 @@ -68,8 +72,11 @@ teardown() { } @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. + # 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; } @@ -77,17 +84,41 @@ teardown() { } @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. + # 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" python "$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; } - [ -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" ] + [ -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 ---- diff --git a/scripts/tests/install-lang-completeness.bats b/scripts/tests/install-lang-completeness.bats new file mode 100644 index 0000000..42832dc --- /dev/null +++ b/scripts/tests/install-lang-completeness.bats @@ -0,0 +1,93 @@ +#!/usr/bin/env bats +# +# Tests for install-lang.sh's bundle-completeness warning. +# +# Background: the python and typescript bundles shipped for nearly two months +# with no githooks/pre-commit, so any project installing them got no +# credential scan on commit. install-lang guarded each component copy with a +# plain `[ -d ... ]`, so a missing component was indistinguishable from a +# complete install — it printed nothing and exited 0. +# +# The fix is not "never miss a component" (a person will), it's "say so when +# you do". These tests pin that: a complete bundle installs quietly, an +# incomplete one names exactly what's absent, and neither case fails the +# install — a warning must not become a new way to block work. + +REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" +INSTALL="$REPO_ROOT/scripts/install-lang.sh" + +setup() { + TEST_DIR="$(mktemp -d -t install-lang-bats.XXXXXX)" + PROJECT="$TEST_DIR/proj" + mkdir -p "$PROJECT" + git init -q "$PROJECT" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +# ---- Normal: a complete bundle is quiet ------------------------------ + +@test "install-lang: a complete bundle warns about nothing" { + run env LANG_ARG=bash bash "$INSTALL" bash "$PROJECT" + [ "$status" -eq 0 ] + [[ "$output" != *"incomplete"* ]] +} + +@test "install-lang: python is now a complete bundle" { + run bash "$INSTALL" python "$PROJECT" + [ "$status" -eq 0 ] + [[ "$output" != *"incomplete"* ]] + [ -f "$PROJECT/githooks/pre-commit" ] + [ -f "$PROJECT/.claude/settings.json" ] + [ -f "$PROJECT/.claude/hooks/validate-python.sh" ] +} + +@test "install-lang: typescript is now a complete bundle" { + run bash "$INSTALL" typescript "$PROJECT" + [ "$status" -eq 0 ] + [[ "$output" != *"incomplete"* ]] + [ -f "$PROJECT/githooks/pre-commit" ] + [ -f "$PROJECT/.claude/settings.json" ] + [ -f "$PROJECT/.claude/hooks/validate-typescript.sh" ] +} + +@test "install-lang: the installed pre-commit is executable" { + run bash "$INSTALL" python "$PROJECT" + [ "$status" -eq 0 ] + [ -x "$PROJECT/githooks/pre-commit" ] +} + +# ---- Error: an incomplete bundle announces itself -------------------- + +@test "install-lang: a bundle missing githooks/ warns and names it" { + fake="$REPO_ROOT/languages/zz-test-partial" + mkdir -p "$fake/claude/rules" + printf '# rule\n' > "$fake/claude/rules/zz.md" + run bash "$INSTALL" zz-test-partial "$PROJECT" + rm -rf "$fake" + [ "$status" -eq 0 ] + [[ "$output" == *"incomplete"* ]] + [[ "$output" == *"githooks/pre-commit"* ]] +} + +@test "install-lang: the warning names every missing component, not just the first" { + fake="$REPO_ROOT/languages/zz-test-partial" + mkdir -p "$fake/claude/rules" + printf '# rule\n' > "$fake/claude/rules/zz.md" + run bash "$INSTALL" zz-test-partial "$PROJECT" + rm -rf "$fake" + [[ "$output" == *"githooks/pre-commit"* ]] + [[ "$output" == *"settings.json"* ]] +} + +@test "install-lang: an incomplete bundle still installs (warn, never block)" { + fake="$REPO_ROOT/languages/zz-test-partial" + mkdir -p "$fake/claude/rules" + printf '# rule\n' > "$fake/claude/rules/zz.md" + run bash "$INSTALL" zz-test-partial "$PROJECT" + rm -rf "$fake" + [ "$status" -eq 0 ] + [ -f "$PROJECT/.claude/rules/zz.md" ] +} diff --git a/scripts/tests/install-lang.bats b/scripts/tests/install-lang.bats index 8518852..c00b915 100644 --- a/scripts/tests/install-lang.bats +++ b/scripts/tests/install-lang.bats @@ -79,19 +79,42 @@ teardown() { grep -qxF "coverage/" "$PROJECT/.gitignore" } -@test "install-lang python: seeds the language-neutral default CLAUDE.md when the bundle ships none" { - run bash "$INSTALL_LANG" python "$PROJECT" +@test "install-lang: seeds the language-neutral default CLAUDE.md when the bundle ships none" { + # Every shipping bundle now carries its own CLAUDE.md (python and typescript + # gained theirs 2026-07-23), so the fallback needs a synthetic bundle to + # exercise. Keep testing it: the fallback is what stops a bundle added later, + # before its CLAUDE.md is written, from inheriting another language's header. + fake="$REAL_REPO/languages/zz-test-noclaude" + mkdir -p "$fake/claude/rules" + printf '# rule\n' > "$fake/claude/rules/zz.md" + run bash "$INSTALL_LANG" zz-test-noclaude "$PROJECT" + rm -rf "$fake" [ "$status" -eq 0 ] [ -f "$PROJECT/CLAUDE.md" ] - # The default names no language, so it can't mislabel a python (or bash, or - # multi-bundle) project the way inheriting elisp's "Elisp project" header did. - ! grep -qi "Python project" "$PROJECT/CLAUDE.md" + # The default names no language, so it can't mislabel a project the way + # inheriting elisp's "Elisp project" header did. ! grep -qi "Elisp project" "$PROJECT/CLAUDE.md" grep -qF "names no language" "$PROJECT/CLAUDE.md" [[ "$output" == *"language-neutral default"* ]] } +@test "install-lang python: seeds the bundle's own CLAUDE.md, not the default" { + run bash "$INSTALL_LANG" python "$PROJECT" + + [ "$status" -eq 0 ] + grep -qF "Python project." "$PROJECT/CLAUDE.md" + [[ "$output" == *"CLAUDE.md installed (python)"* ]] +} + +@test "install-lang typescript: seeds the bundle's own CLAUDE.md, not the default" { + run bash "$INSTALL_LANG" typescript "$PROJECT" + + [ "$status" -eq 0 ] + grep -qF "TypeScript/JavaScript project." "$PROJECT/CLAUDE.md" + [[ "$output" == *"CLAUDE.md installed (typescript)"* ]] +} + @test "install-lang elisp: seeds the bundle's own CLAUDE.md, not the default" { run bash "$INSTALL_LANG" elisp "$PROJECT" @@ -147,3 +170,32 @@ teardown() { grep -qxF ".claude/" "$PROJECT/.gitignore" grep -qxF "cover.out" "$PROJECT/.gitignore" } + +@test "install-lang python: full bundle lands (rules, hook, settings, githook, CLAUDE.md, coverage)" { + run bash "$INSTALL_LANG" python "$PROJECT" + + [ "$status" -eq 0 ] + [ -f "$PROJECT/.claude/rules/python-testing.md" ] + # PostToolUse validate hook, executable and wired into settings + [ -x "$PROJECT/.claude/hooks/validate-python.sh" ] + grep -qF "validate-python.sh" "$PROJECT/.claude/settings.json" + # Pre-commit githook — the secret scan. Absent until 2026-07-23. + [ -x "$PROJECT/githooks/pre-commit" ] + grep -qF "potential secret" "$PROJECT/githooks/pre-commit" + # Coverage slice + [ -f "$PROJECT/.claude/scripts/coverage-summary.py" ] + grep -qxF ".claude/" "$PROJECT/.gitignore" +} + +@test "install-lang typescript: full bundle lands (rules, hook, settings, githook, CLAUDE.md, coverage)" { + run bash "$INSTALL_LANG" typescript "$PROJECT" + + [ "$status" -eq 0 ] + [ -f "$PROJECT/.claude/rules/typescript-testing.md" ] + [ -x "$PROJECT/.claude/hooks/validate-typescript.sh" ] + grep -qF "validate-typescript.sh" "$PROJECT/.claude/settings.json" + [ -x "$PROJECT/githooks/pre-commit" ] + grep -qF "potential secret" "$PROJECT/githooks/pre-commit" + [ -f "$PROJECT/.claude/scripts/coverage-summary.js" ] + grep -qxF ".claude/" "$PROJECT/.gitignore" +} |
