From 921598f30e470482efd3ce4981ee9893c8be60cf Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 24 Jul 2026 10:58:25 -0500 Subject: fix(test): stop the syncing-p tests leaking in-flight state - They cleared the module's state hash on entry, not on exit. - One marks "proton" syncing to exercise the dispatcher's in-flight guard. - A sibling dispatch test uses a calendar also named "proton". - ERT runs them in that order, so the dispatch became a no-op and it failed. - Now each test let-binds a private hash, matching the sibling test files. - Added a test that pins the isolation itself. The failure was invisible from both directions. make test runs each file in its own Emacs, and the editor hook skipped this family for exceeding its file cap. It only appears when the 63 calendar-sync files run in one process. --- tests/test-validate-el-hook.bats | 86 +++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 31 deletions(-) (limited to 'tests/test-validate-el-hook.bats') diff --git a/tests/test-validate-el-hook.bats b/tests/test-validate-el-hook.bats index 2dbcae79..43c3569c 100644 --- a/tests/test-validate-el-hook.bats +++ b/tests/test-validate-el-hook.bats @@ -1,19 +1,21 @@ #!/usr/bin/env bats -# Tests for .claude/hooks/validate-el.sh — specifically the auto-test cap. +# Tests for .claude/hooks/validate-el.sh — the auto-test runner. # -# The hook runs the tests matching an edited file, but only when the match -# count is between 1 and MAX_AUTO_TEST_FILES. Above the cap the whole block -# was skipped with no else branch: nothing printed, exit 0, indistinguishable -# from a passing run. That is live for the three largest families here -# (calendar-sync 63 test files, music 45, ai-term 35), so every edit to those -# modules ran parens and byte-compile and zero tests, silently. +# The runner used to skip entirely above MAX_AUTO_TEST_FILES=20, with no else +# branch: nothing printed, exit 0, indistinguishable from a passing run. That +# was live for the three largest families here (calendar-sync 63 test files, +# music 45, ai-term 35), so every edit to those ran parens and byte-compile and +# zero tests, silently. # -# The cap itself is fine — running 63 files per keystroke is not wanted. The -# defect is the silence, so these tests assert the skip announces itself and -# names what to run. +# The cap was removed rather than made loud, because its premise did not hold. +# Measured on this machine, running a whole family takes about a second: +# ai-term 208 tests in 1.0s, music 403 in 1.7s, calendar-sync 633 in 0.9s. It +# was also concealing a real cross-test pollution bug in calendar-sync that +# only appears when that family runs in one process. # -# Each test builds a synthetic project in BATS_TEST_TMPDIR and points -# CLAUDE_PROJECT_DIR at it, so nothing runs against the real tree. +# These tests pin that no file count is skipped. Each builds a synthetic +# project in BATS_TEST_TMPDIR and points CLAUDE_PROJECT_DIR at it, so nothing +# runs against the real tree. setup() { HOOK="${BATS_TEST_DIRNAME}/../.claude/hooks/validate-el.sh" @@ -23,8 +25,7 @@ setup() { printf '(provide (quote widget))\n' > "$PROJ/modules/widget.el" } -# Create N test files matching the widget stem. Each is trivially green so a -# run below the cap succeeds and the only variable is the count. +# N green test files matching the widget stem. make_tests() { local n="$1" i for ((i = 1; i <= n; i++)); do @@ -33,41 +34,64 @@ make_tests() { done } +# One failing test file, to prove the run is real rather than merely quiet. +make_failing_test() { + printf '(require (quote ert))\n(ert-deftest test-widget-bad () (should nil))\n' \ + > "$PROJ/tests/test-widget-bad.el" +} + hook_input() { printf '{"tool_input":{"file_path":"%s"}}' "$PROJ/modules/widget.el" } +run_hook() { + run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'" +} + # ------------------------------- Normal cases ------------------------------- -@test "under the cap: runs the tests and stays quiet on success" { +@test "a small family runs and passes quietly" { make_tests 3 - run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'" + run_hook [ "$status" -eq 0 ] - [[ "${output,,}" != *"skipped"* ]] +} + +@test "a failing test blocks, so a quiet pass means the tests really ran" { + make_tests 3 + make_failing_test + run_hook + [ "$status" -eq 2 ] + [[ "$output" == *"TESTS FAILED"* ]] } # ------------------------------ Boundary cases ------------------------------ -@test "exactly at the cap: still runs the tests" { +@test "at the old cap of 20 files: runs" { make_tests 20 - run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'" + run_hook [ "$status" -eq 0 ] - [[ "${output,,}" != *"skipped"* ]] } -# -------------------------------- Error cases ------------------------------- - -@test "over the cap: says it skipped rather than exiting silently" { +@test "past the old cap: still runs, no longer skipped" { make_tests 21 - run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'" - # Must not fail the edit — the cap is deliberate, the silence is not. + run_hook [ "$status" -eq 0 ] - [[ "${output,,}" == *"skipped"* ]] + [[ "${output,,}" != *"skipped"* ]] } -@test "over the cap: names the count and how to run them" { - make_tests 21 - run bash -c "$(printf '%q' "$HOOK") <<< '$(hook_input)'" - [[ "$output" == *"21"* ]] - [[ "$output" == *"make test-file"* ]] +@test "well past the old cap: a failure in file 63 is still caught" { + # The regression this guards: at 63 files the runner used to skip, so a red + # test in a big family reported clean. calendar-sync is exactly this size. + make_tests 63 + make_failing_test + run_hook + [ "$status" -eq 2 ] + [[ "$output" == *"TESTS FAILED"* ]] +} + +# -------------------------------- Error cases ------------------------------- + +@test "no matching tests: exits clean without running anything" { + run_hook + [ "$status" -eq 0 ] } -- cgit v1.2.3