Files
FabledScribe/tests/test_hook_pipefail_head.py
T
bvandeusenandClaude Opus 5 0fab08276c
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 14s
fix(plugin): an early-exiting head no longer voids its own output under pipefail (#4042)
In a repo where 3,000 files define `slug`, scribe_local_dups printed
nothing; with 3 files it printed the duplicate line. Every hook runs under
`set -uo pipefail`, and `hits=$(git grep -l … | head -4) || hits=""` lost
head's four lines whenever git grep was still writing when head exited. That
is a SIGPIPE, the substitution fails, and the outer fallback wipes the result.
So the by-name duplicate arm went silent for exactly the most-duplicated
names. Found while fixing the same trap in the new Stop hook (#4041).

- The fallback moves inside the substitution, `$(… | head -N || true)`, at all
  five sites: scribe_defs.sh (local dups), scribe_prior_art.sh (names,
  old_first, shapes) and scribe_after_write.sh (names). A real upstream
  failure still yields empty output.
- check_plugin gains a known-bad pattern for the shape, so no hook can bring
  it back.
- Tests: the real function against a 3,000-file repo (past the pipe buffer),
  and the pattern shown to flag the old shape and pass the fix. Plugin
  version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 18:54:49 -04:00

60 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Under `pipefail`, an early-exiting `head` must not void the output it kept (#4042).
The hooks run with `set -uo pipefail`. `hits=$(git grep -l … | head -4) ||
hits=""` lost its four hits whenever git grep was still writing when head
exited: SIGPIPE, a failed substitution, and the fallback wiped the result. It
only showed on big outputs, so the by-name duplicate arm went silent for
exactly the most-duplicated names.
Two halves: the real function on a repo big enough to overflow the pipe, and
the check_plugin pattern that keeps the shape out of every hook (shown able to
fail, rule #167).
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
DEFS = ROOT / "plugin" / "hooks" / "scribe_defs.sh"
def test_the_duplicate_arm_still_names_a_name_defined_in_thousands_of_files(tmp_path):
for tool in ("git", "bash"):
if shutil.which(tool) is None:
pytest.skip(f"{tool!r} not installed")
env = {"PATH": os.environ["PATH"], "HOME": str(tmp_path),
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@x",
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@x"}
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True, env=env)
# ~3000 × ~60 bytes of `git grep -l` output: well past a 64 KiB pipe buffer,
# so git grep is still writing when head has its four lines.
for i in range(3000):
(repo / f"module_with_a_fairly_long_descriptive_name_{i}.py").write_text("def slug(t):\n return t\n")
subprocess.run(["git", "add", "."], cwd=repo, check=True, env=env)
subprocess.run(["git", "commit", "-q", "-m", "base"], cwd=repo, check=True, env=env)
script = f'set -uo pipefail\n. "{DEFS}"\nprintf "sym\\tslug\\n" | scribe_local_dups "{repo}" new.py\n'
out = subprocess.run(["bash", "-c", script], capture_output=True, text=True, env=env, timeout=60)
assert out.returncode == 0, out.stderr
assert "`slug` is already defined in 4 other file(s)" in out.stdout
def test_the_lint_pattern_catches_the_shape_and_passes_the_fix():
from scripts.check_plugin import PATTERNS
pattern = next(p for p, label, _why in PATTERNS if label.startswith("early-exit consumer"))
for bad in ('hits=$(git grep -l x | head -4) || hits=""',
" | grep -m1 -v '^[[:space:]]*$') || old_first=\"\"",
'x=$(a | grep -q b) || x=""'):
assert pattern.search(bad), bad
for good in ('hits=$(git grep -l x | head -4 || true)',
'ln=$(grep -nF -m1 -- "$a" "$f" | cut -d: -f1) || ln=""'):
assert not pattern.search(good), good