fix(tests): replace pipefail-racy echo | grep -q with here-strings
Why Two suites failed intermittently — tests/test-vale-wrap.sh case 21 and tests/test-check-release-needed.sh cases 4 and 15 — on correct output, and never when run alone. The cause is the `echo "$OUT" | grep -q P` idiom under `set -o pipefail`: grep -q exits as soon as it has an answer, bash's echo can hand a multi-line value to the pipe one line at a time, and a write after the reader is gone kills echo with SIGPIPE. pipefail then reports the writer's death, so output that DID match reads as "no match". Every observed failure had lines after its match; case 15's match is on line 1 of 6, the widest window in that file. Forced with a pause before the writer's last line, the pipe form failed 50 of 50 runs; a here-string, a match on the last line, and the same pipe without pipefail each passed 50 of 50. Unforced the rate is about 1 per 670 suite runs, which is why it read as a flaky gate rather than a bug. The failures at review time are consistent with this, but were not proven to be it: the suite was running while agents edited live config files in place, and a brief change to .vale.ini or .pre-commit-hooks.yaml would produce the same two failures. The race is real and fixed either way. Implementation Notes `grep -q P <<< "$VAR"` has no separate writer process, so there is nothing to race. It is not a retry or a sleep. 121 sites converted across 9 files, three of them scripts rather than tests: new-agent.sh, new-skill.sh and check-executables-allow-sync.sh. None ships via .pre-commit-hooks.yaml, so no external consumer pins them, and all three are single-pipeline checks whose verdict cannot change. Left alone deliberately: 14 sites whose writer is a command, not a shell builtin — they either absorb the writer's status with `|| true` or are python3 and awk, which write once at exit — and one file with no pipefail. `printf '%s'` sites differ from a here-string only by a trailing newline, which no -q verdict on a non-empty pattern depends on. tests/test-no-pipefail-early-exit-grep.sh is a static guard against new occurrences, discovered automatically by run-tests.sh. It only scans files that set pipefail, joins continuation lines, skips comments, and flags only echo/printf writers. Its first case proves the scanner can fail before its second trusts a clean verdict on the tree. A guard covers exactly the spellings its regex models, so the miss surface was measured rather than assumed. Four were found and closed: pipefail declared as `set -o errexit -o pipefail` (where the old pattern required pipefail to follow the FIRST -o, and a file-level miss skips every site in that file); a writer separated from grep by an intermediate stage; a pipeline wrapped on a trailing `|` rather than a backslash; and readers spelled egrep, fgrep, /bin/grep, `command grep` or with an env-var prefix. Segment characters exclude a bare `&` so `echo ok && other | grep -q x`, whose writer is `other`, does not false-fire. Widening surfaced 5 live sites invisible to the original scanner, all in tests/test-apm-current-hook.sh, all `echo "$out" | json_field ... | grep -q`; they are safe today only because json_field is python3, which reads to EOF and writes once. Fixtures go 4 to 12 vulnerable spellings plus near-miss negatives. Two `grep ... | head -1` sites (test-vale-wrap.sh) are the same race with a different early-exiting reader, and are fixed by absorbing the writer. The scanner deliberately does not model `head`, `sed -n 1p` or a bare `read`: most legitimate uses in this tree are already absorbed with `|| true` and the scanner cannot see absorption from pipeline text, so a high false-positive rate would be how this guard gets weakened. Heredoc bodies are scanned as code; none in the tree trips it today. Impact The bug predates the factory-audit merge: every converted site in check-release-needed and case 21 dates to4d018afandaa8cc22(2026-08-09). Test suites go 19 to 20. `run-tests.sh --strict` passes 20/20 with 0 skipped, four consecutive runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
This commit is contained in:
@@ -112,7 +112,7 @@ EOF
|
||||
run_fake "$DIR1"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "a bats emitting nothing exited 0 — a total harness failure reported as a pass"
|
||||
elif echo "$FAKE_OUT" | grep -q "no TAP output at all"; then
|
||||
elif grep -q "no TAP output at all" <<< "$FAKE_OUT"; then
|
||||
pass "an empty TAP stream fails the run and names the broken harness"
|
||||
else
|
||||
fail "the run failed but not with the broken-harness message: $FAKE_OUT"
|
||||
@@ -134,9 +134,9 @@ EOF
|
||||
run_fake "$DIR2"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "every test declaring zero tests exited 0 — wholesale @test removal reported as a pass"
|
||||
elif echo "$FAKE_OUT" | grep -q "declared 0 tests"; then
|
||||
elif grep -q "declared 0 tests" <<< "$FAKE_OUT"; then
|
||||
pass "a plan-only TAP stream fails the run and names the removed tests"
|
||||
elif echo "$FAKE_OUT" | grep -q "no TAP output at all"; then
|
||||
elif grep -q "no TAP output at all" <<< "$FAKE_OUT"; then
|
||||
fail "a valid '1..0' plan was misreported as a broken harness — the two branches are not distinguished"
|
||||
else
|
||||
fail "the run failed but not with the no-tests-declared message: $FAKE_OUT"
|
||||
@@ -160,7 +160,7 @@ EOF
|
||||
run_fake "$DIR3"
|
||||
if [[ $FAKE_RC -ne 0 ]]; then
|
||||
fail "a healthy TAP stream was failed by the zero-count guard: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^4 tests, 0 failures$"; then
|
||||
elif grep -q "^4 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "two files reporting two passing tests each aggregate to 4 tests, 0 failures"
|
||||
else
|
||||
fail "a healthy TAP stream produced the wrong count: $FAKE_OUT"
|
||||
@@ -187,9 +187,9 @@ else
|
||||
run_fake "$DIR4"
|
||||
if [[ $FAKE_RC -ne 0 ]]; then
|
||||
fail "an empty .bats file beside a real one failed the run: $FAKE_OUT"
|
||||
elif ! echo "$FAKE_OUT" | grep -q "^1\.\.0$"; then
|
||||
elif ! grep -q "^1\.\.0$" <<< "$FAKE_OUT"; then
|
||||
fail "real bats did not emit '1..0' for an empty file, so the case-2 stub no longer matches it: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^1 tests, 0 failures$"; then
|
||||
elif grep -q "^1 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "an empty .bats file contributes a '1..0' plan and the suite still passes"
|
||||
else
|
||||
fail "the real-bats run passed with an unexpected count: $FAKE_OUT"
|
||||
@@ -215,7 +215,7 @@ EOF
|
||||
run_fake "$DIR5"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "a 'not ok' TAP result exited 0 — a failing test reported as a pass because only the process status was consulted"
|
||||
elif echo "$FAKE_OUT" | grep -q "^2 tests, 2 failures$"; then
|
||||
elif grep -q "^2 tests, 2 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "'not ok' lines fail the run even when every bats process exits 0"
|
||||
else
|
||||
fail "the run failed but with the wrong count: $FAKE_OUT"
|
||||
@@ -242,7 +242,7 @@ EOF
|
||||
run_fake "$DIR6"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "a bats process exiting 1 was reported as a pass because only the TAP text was consulted"
|
||||
elif echo "$FAKE_OUT" | grep -q "^2 tests, 0 failures$"; then
|
||||
elif grep -q "^2 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "a non-zero bats exit fails the run even with no 'not ok' line to find"
|
||||
else
|
||||
fail "the run failed but with the wrong count: $FAKE_OUT"
|
||||
@@ -282,7 +282,7 @@ run_fake "$DIR7"
|
||||
# the stated reason: the TAP stream the stub emitted is healthy, so "1 tests, 0
|
||||
# failures" proves the zero-count guard did not fire and the empty status is the
|
||||
# only thing left that can have failed the run.
|
||||
if ! echo "$FAKE_OUT" | grep -q "^1 tests, 0 failures$"; then
|
||||
if ! grep -q "^1 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
fail "the killed job did not leave the healthy TAP stream the case needs: $FAKE_OUT"
|
||||
elif [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "an empty status file was counted as a clean exit — a killed job reported as a pass"
|
||||
@@ -312,9 +312,9 @@ rm "$DIR8/tests/b.bats"
|
||||
run_fake "$DIR8"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "a tracked .bats file gone from the worktree passed — a deleted suite reads as green"
|
||||
elif ! echo "$FAKE_OUT" | grep -q "tracked .bats file(s) were not discovered"; then
|
||||
elif ! grep -q "tracked .bats file(s) were not discovered" <<< "$FAKE_OUT"; then
|
||||
fail "the run failed but not with the undiscovered-tracked-file message: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^ tests/b.bats$"; then
|
||||
elif grep -q "^ tests/b.bats$" <<< "$FAKE_OUT"; then
|
||||
pass "a tracked .bats file missing from the walk fails the run and names the file"
|
||||
else
|
||||
fail "the run failed without naming the missing file: $FAKE_OUT"
|
||||
@@ -339,7 +339,7 @@ git -C "$DIR8B" add tests/a.bats
|
||||
run_fake "$DIR8B"
|
||||
if [[ $FAKE_RC -ne 0 ]]; then
|
||||
fail "an untracked .bats file was reported as a finding: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^2 tests, 0 failures$"; then
|
||||
elif grep -q "^2 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "an untracked .bats file is run without being demanded of the index"
|
||||
else
|
||||
fail "the untracked-file run passed with the wrong count: $FAKE_OUT"
|
||||
@@ -356,7 +356,7 @@ rm "$DIR8B/tests/b.bats"
|
||||
run_fake "$DIR8B"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "the .bats file added to the index a moment ago was not demanded back: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^ tests/b.bats$"; then
|
||||
elif grep -q "^ tests/b.bats$" <<< "$FAKE_OUT"; then
|
||||
pass "a file added to the index joins the expected set with no floor to bump"
|
||||
else
|
||||
fail "the run failed but did not name the newly tracked file: $FAKE_OUT"
|
||||
@@ -380,9 +380,9 @@ EOF
|
||||
run_fake "$DIR8D"
|
||||
if [[ $FAKE_RC -ne 0 ]]; then
|
||||
fail "a non-git tree failed the run: $FAKE_OUT"
|
||||
elif ! echo "$FAKE_OUT" | grep -q "not a git worktree root"; then
|
||||
elif ! grep -q "not a git worktree root" <<< "$FAKE_OUT"; then
|
||||
fail "a non-git tree silently skipped the derived expectation with no note: $FAKE_OUT"
|
||||
elif echo "$FAKE_OUT" | grep -q "^2 tests, 0 failures$"; then
|
||||
elif grep -q "^2 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "a non-git tree runs the suite and says the expected set could not be derived"
|
||||
else
|
||||
fail "the non-git run passed with the wrong count: $FAKE_OUT"
|
||||
@@ -404,7 +404,7 @@ EOF
|
||||
run_fake "$DIR9"
|
||||
if [[ $FAKE_RC -eq 0 ]]; then
|
||||
fail "finding no .bats files at all exited 0 — the whole suite can vanish and the run stays green"
|
||||
elif echo "$FAKE_OUT" | grep -q "found 0 .bats file"; then
|
||||
elif grep -q "found 0 .bats file" <<< "$FAKE_OUT"; then
|
||||
pass "finding no .bats files fails the run and says so"
|
||||
else
|
||||
fail "the run failed but not with the zero-files message: $FAKE_OUT"
|
||||
|
||||
Reference in New Issue
Block a user