Repository navigation
fix(hooks): hooks that never run, and the guards that cannot see them (#14909, #14891, #14884) - #14916
Merged
Conversation
…14909) .git/hooks/pre-push was a dangling symlink into a worktree deleted in April, and repointing it was not enough: tools/git-hooks/pre-push, pre-commit and install_hooks.sh were all tracked 100644, and git silently skips a hook that is not executable. core.fileMode=false hides that locally — a plain chmod +x never reaches the index, so git status stays clean while every fresh clone gets an inert hook. Modes set with git update-index --chmod=+x (100644 -> 100755): tools/git-hooks/pre-push tools/git-hooks/pre-commit tools/git-hooks/install_hooks.sh scripts/install-git-hooks.sh autobot-infrastructure/shared/scripts/hooks/function_length_checker.py slm-post-commit carried its #!/bin/bash on line 4, below the copyright header, where it is a comment — execve refuses the file and git falls back to sh, so a bash-only hook changed meaning silently. Moved to line 1. repo_tests/git_hooks_executable_test.py reads the index mode via git ls-files -s (never os.access, which reports the locally patched working file) and asserts a derived rule in both directions: a file that declares a shebang must be 100755, and a file that is 100755 must declare a shebang. Four reach floors, including an explicit extensionless count, so an empty sweep fails instead of reading as clean. The README's "the hooks are symlinked" section was stale since #11598 made the installer copy-based; it now says to re-run the installer and documents the update-index spelling.
… hooks (#14891) repo_tests/shell_lib_sources_resolve_test.py walked _INFRA.rglob("*.sh"). Every pre-commit hook under autobot-infrastructure/shared/scripts/hooks/ is extensionless, so 16 real call sites sat outside the guard — and its three reach floors were all satisfied by the .sh population alone, so it reported healthy coverage over its own blind spot. That is the defect it exists to catch, one level up. Detection is now by shebang, not by suffix or a name list. Measured on this branch: 160 -> 179 shell scripts walked, 61 -> 78 lib source sites, 55 -> 71 distinct scripts, 56 -> 73 decisive sites. Every floor is re-derived AT the new count, and a new floor asserts the extensionless population specifically so a regression to rglob("*.sh") cannot pass on the .sh files alone. Widening needed a third category rather than a wider glob. test_each_failure_block_actually_exits required `exit [1-9]`, but pre-commit-warn-untracked's documented contract is that it must never block a commit, so its block correctly ends in `exit 0`. That is now an asserted category, not an exemption: the block has to carry a `lib-source-contract: non-blocking` marker, announce the skip on stderr, and exit 0 explicitly. The category is bounded above so it cannot become the escape hatch that turns the whole guard green. Both live offenders the widened walk found are fixed: post-commit-doc-sync:16 sourced with no failure block at all — it failed closed only because of the incidental `set -e` at the top of the file. pre-commit-warn-untracked:49 wrote the decisive attempt as `if ! source ... 2>/dev/null`, discarding the library's own stderr, so a developer was told the load failed but never why.
Two different causes behind one symptom — 8 tests that could not verify the property they name. pre-commit-worktree-branch-guard_test.py resolved git with subprocess.run(["command", "-v", "git"]). `command` is a POSIX shell BUILTIN, and subprocess given a list never involves a shell, so it raised FileNotFoundError wherever no `command` binary sits on PATH — before the hardcoded candidate list below it ever ran, and its result was never read. The test that died is the fail-closed proof that a `git worktree list` failure is not reported as a clean tree, so that property was unverified. Replaced with shutil.which, which answers the same question in-process and honours PATH. pre-commit-no-tag-pinned-action_test.py failed for a different reason than #14884 recorded: its 7 failures are AssertionError (0 == 1), not FileNotFoundError. The harness passed an ABSOLUTE /tmp/pytest-.../ path in argv. #13936 made get_staged_files apply the caller's own pattern to the argv branch, and this hook's pattern is anchored ^\.github/(workflows|actions)/ — which an absolute path cannot match. The hook received an empty file list and took its "nothing staged" exit 0. The 7 "must reject" tests read that as a missing rejection; worse, the 3 "must allow" tests were passing vacuously, having exercised nothing. The path now goes in repo-relative with cwd at its root, which is the only shape pre-commit or a CI wrapper produces. repo_tests/hook_suite_harness_test.py answers the issue's last acceptance criterion — whether other suites share the idiom — by AST, not by grep, so a builtin named in a docstring is not a false positive. 18 modules, 67 spawn sites, no other offender. Reach floors included, because an AST walk that stops matching finds nothing and reads exactly like a clean tree.
#11598 rewrote the installer to copy real files instead of symlinking into a worktree, but nothing asserted the property, so the only evidence the class was closed was that someone had intended to close it — and #14909 found .git/hooks/pre-push still pointing into a worktree deleted in April. Drives the real installer from inside a real linked worktree, removes the worktree, and checks the hook is still a file, still executable, and still byte-identical to its template. A second case starts from the state the repository was actually found in — a dangling symlink at the destination — and checks a re-run replaces it. Asserted on behaviour, not on the installer's source: a grep for `ln -s` passes on an installer that has stopped installing anything, which is the same "absent reads as clean" shape as the bug.
Fixing slm-post-commit's shebang in the first commit of this PR (#14909 — it sat on line 4, under the copyright header, where bash reads it as a comment) also made that file visible to the shebang-based walk. Measured after both changes: 180 shell scripts, not the 179 counted before the shebang moved. The site counts are unchanged at 78/71/73 because slm-post-commit carries no lib source. Floor moved to the real number so it still catches a regressed walk rather than carrying a spare unit of headroom.
…rds read (#14909) python-suite is gated by .github/filters/python-paths.yml, and everything the three new guards assert on is a shell script or a file MODE — no Python at all. A PR that flipped tools/git-hooks/pre-push back to 100644, or put `|| true` back into an extensionless hook, would have matched no pattern in the filter: the shim computes `python != 'true'`, publishes the required python-suite context green, and the guard written to catch exactly that never runs. The guards would have been inert on the only diffs they exist for. This is the shape the filter's own header already documents for #14544/#14575, so the fix is the same: name the inputs. A mode-only change still appears in the changed-file list, so these entries do gate it. test_the_python_filter_reaches_the_inputs_these_guards_read evaluates the globs against real paths instead of looking for the strings, because an entry that is present but no longer matches anything is worth nothing — verified by mutation in both directions (entries removed, and one entry misspelled while still present).
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
…14884) Review finding, and it is correct: fixing the two harness defects made the 8 tests pass when invoked by hand, and nothing else. They still ran nowhere. pytest.ini testpaths — 8 roots, autobot-infrastructure absent ci.yml, coverage.yml, test-durations.yml — explicit path lists, same omission autobot-infrastructure/shared/tests/ pytest.ini — testpaths = unit integration e2e, which does not reach shared/scripts/ So all 18 suites under shared/scripts/hooks/ were collected by nothing, and the fail-closed proof for `git worktree list` that #14884 exists to restore was verified exactly once, by hand, in the PR claiming to restore it. That is the same "absent reads as clean" shape this PR closes twice elsewhere. Wired into all four places, not just ci.yml — the omission was identical in each and fixing one leaves the drift. NARROW ON PURPOSE. `autobot-infrastructure` as a whole does not collect: 23 collection errors, and shared/scripts/test_alertmanager.py calls sys.exit(0) at import time, which takes pytest down with an INTERNALERROR rather than a test failure. Filed separately instead of bundled here. Measured before wiring, so this is a wiring fix and not a quarantine: 159 collected, 159 passed under this repo's own pytest.ini (importlib import mode, strict markers). No package-name collision of the #13084 kind — the suites import only stdlib plus pytest, carry no __init__.py, and need no separate interpreter invocation. repo_tests/hook_suites_run_in_ci_test.py guards both halves, because either alone passes while the wiring is broken: the configuration half derives the invocation set by scanning the workflows for `python -m pytest` rather than naming three files, so a fourth workflow with the same omission fails too; the behaviour half runs the real collector under this repo's pytest.ini, because a path listed in a config that then ignores it collects nothing and exits zero. Also folds in the review's structural note. python-paths.yml now says `autobot-infrastructure/**` rather than two shell-shaped patterns: shell_lib_sources_resolve_test.py finds its subjects by SHEBANG, and "has a shebang naming a shell" cannot be written as a glob, so a new extensionless script added elsewhere under that tree would have matched neither pattern and taken the shim's green. The filter test's subjects are now derived from `git ls-files` over the roots the guards actually read (646 non-Python files) instead of three hand-picked paths.
…pelling (#14884) The guard caught its own author. `python-suite shard 2/12` went red on 53f4d30 naming marker-tests.yml — a FOURTH invocation that runs repo_tests without the hook suites. The review named three workflows; the derived sweep found the one the list did not have. Except the sweep was only half-derived, and the failure showed why. It matched `python -m pytest` and nothing else. Measured across the CI tree: 27 bare `pytest`, 19 `python -m pytest`, 1 `python3 -m pytest` — so 44 of 64 invocations were invisible to it, and it reached marker-tests.yml only because that one happens to use the middle spelling. A guard narrower than its own subject reads as coverage. Now derived on three axes instead of one: * all three invocation spellings, not the one this repo happens to use most * .yml AND .yaml * .github/actions/ composite actions as well as .github/workflows/ 64 invocations across 61 files; exactly 4 name repo_tests as a bare path token (ci.yml, coverage.yml, marker-tests.yml, test-durations.yml). `-p repo_tests.stable_shard` is correctly not one of them. The carrier floor sits AT 4, and the invocation floor at 40 catches a scanner that has stopped matching — mutation-proved: narrowing the regex back to the single spelling fails on `only found 19 pytest invocations`, rather than passing with an empty offender list. The reviewer also flagged a PytestReturnNotNoneWarning in the same log. It is not from this branch: backend_startup_e2e_test.py::test_imports returns a bool. Counted by AST across the repo — attributing a return only to the test that directly encloses it, since a naive walk inflates 262 to 739 — there are 262 such tests, 230 of them in autobot-backend, and ZERO in the directory this PR wires into CI. A test that ends in `return False` still passes, so the class is the same one this PR closes. Filed as #14920 rather than bundled.
…filename (#14884) Wiring marker-tests.yml was the wrong fix, and CI said so: it went red with 20 failed / 19 errors, every one of them `fixture 'backend_url' not found` in autobot-backend/services/redis_service_api_integration_test.py. Nothing to do with hooks. That workflow documents itself as a KNOWN-RED suite deliberately kept off the PR gate until #13286 triages it, and it carries `pull_request: paths: ['.github/workflows/marker-tests.yml']` precisely so that editing it runs it. So my edit dragged an unrelated red suite into this PR's gate and bought nothing. Nothing, because marker-tests.yml runs `-m "integration or slow or distributed or performance"` — a POSITIVE selection — and no hook test carries any of those four markers. Adding the path there collects 18 modules and selects zero tests from them. The invariant was too coarse. It is not "every invocation that names repo_tests" but "every invocation that can SELECT the hook suites", and that is readable from each invocation's own `-m` expression: ci.yml -m 'not integration and not slow and ...' selects unmarked -> needs the path coverage.yml -m 'not integration and not slow and ...' selects unmarked -> needs the path test-durations -m 'not integration and not slow and ...' selects unmarked -> needs the path marker-tests -m 'integration or slow or ...' marked only -> exempt `$MARKER_EXPRESSION` is resolved from the env block in its own file rather than guessed, and an expression that CANNOT be resolved is a hard failure, never an exemption — unknown must not read the same as "does not need it". The exemption is EARNED, not asserted. The guard re-derives whether any hook test carries one of the four markers; the moment one does, marker-tests.yml stops being exempt and is named. Mutation-proved: adding `@pytest.mark.integration` to a hook test fails with that file and line. Also switched argv parsing from a naive whitespace split to shlex, so a quoted `-m` expression is one token instead of six fragments. marker-tests.yml is restored byte-identical to base (blob 6281b27), so this PR no longer triggers that workflow at all.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14909, #14891, #14884
Three findings, one root cause: hook files are tracked without the executable bit, and
core.fileMode=falsehides that from every local check. Fixing the modes without widening the guards that should have caught them would leave the class open, so all three land together.Thinking Path
The three issues describe different symptoms — a dangling symlink, a
*.shglob, aFileNotFoundError— but they are one shape: a check that is absent reads exactly like a check that passed.core.fileMode=falseis the mechanism behind the first. A plainchmod +xmakes the working file executable and never reaches the index;git statusstays clean, the local hook runs, and every other clone gets100644— which git skips silently. So the fix has to begit update-index --chmod=+x, and the guard has to readgit ls-files -s. A guard usingos.accessorstatwould have reported the locally-patched working file and passed on a repository that was still broken, which is the same failure one level up.Widening #14891's walk was not a one-line glob change.
test_each_failure_block_actually_exitsrequiresexit [1-9], butpre-commit-warn-untracked's documented contract is that it must never block a commit, so its block correctly ends inexit 0. A filename allowlist would have made the guard's reach a maintained list whose failure mode is silence. Instead the third category is earned from the block itself — alib-source-contract: non-blockingmarker, a message on stderr, an explicitexit 0— and bounded above so it cannot become the escape hatch that turns the whole guard green.The reach floors mattered more than the glob.
_SCANNED > 120,len(_SITES) >= 60and>= 55distinct scripts were all satisfied by the.shpopulation alone, so the guard reported healthy coverage over its own blind spot. Every floor is re-derived at the widened count, plus a new one that only an extensionless script can satisfy — so a regression torglob("*.sh")fails on something the.shfiles cannot rescue.One correction to #14884. The issue records both suites failing with
FileNotFoundError: 'command'. That is true of the worktree-guard test only.pre-commit-no-tag-pinned-action_test.py's 7 failures areAssertionError: 0 == 1, from a different cause: the harness passed an absolute/tmp/pytest-.../path in argv, #13936 madeget_staged_filesapply the caller's own anchored pattern (^\.github/(workflows|actions)/) to the argv branch, and an absolute path cannot match that anchor. The hook received an empty file list and took its "nothing staged"exit 0. Worse than the 7 reds: the 3 "must allow" tests in that file were passing vacuously, having exercised nothing at all — mutation E below shows they now go red when the hook stops allowing first-party actions, which they could not do before.The wiring guard caught its own author, twice. The first version of
hook_suites_run_in_ci_test.pyderived its invocation set by scanning the workflows — and immediately went red onmarker-tests.yml, a fourth invocation neither the review nor I had listed. Then the failure exposed that the sweep was only half-derived: it matchedpython -m pytestand nothing else, while the CI tree actually contains 27 barepytest, 19python -m pytestand 1python3 -m pytest. It reachedmarker-tests.ymlonly because that file happens to use the middle spelling — 44 of 64 invocations were invisible to it. A guard narrower than its own subject reads as coverage. It now derives on three axes: all three spellings,.ymland.yaml, and.github/actions/composite actions as well as.github/workflows/.Then wiring
marker-tests.ymlturned out to be the wrong fix, and CI said so — 20 failed / 19 errors, every onefixture 'backend_url' not foundin a backend integration test, nothing to do with hooks. That workflow documents itself as a known-red suite kept off the PR gate until #13286, and carriespull_request: paths: ['.github/workflows/marker-tests.yml']precisely so editing it runs it. My edit dragged an unrelated red suite into this PR's gate and bought nothing — because it runs-m "integration or slow or distributed or performance", a positive selection, and no hook test carries any of those four markers. Adding the path there collects 18 modules and selects zero tests.So the invariant was too coarse. It is not "every invocation that names
repo_tests" but "every invocation that can select the hook suites", which is readable from each invocation's own-mexpression.$MARKER_EXPRESSIONis resolved from the env block in its own file, and an expression that cannot be resolved is a hard failure rather than an exemption — unknown must not read the same as "does not need it". The exemption is earned: the guard re-derives whether any hook test carries one of the four markers, and the moment one does,marker-tests.ymlstops being exempt.marker-tests.ymlis restored byte-identical to base (blob6281b27ca), so this PR no longer triggers it.Review finding, and it was right. "The 8 tests now run" was true only by hand.
pytest.ini'stestpathslisted 8 roots andautobot-infrastructurewas in none of them; neither was it inci.yml,coverage.ymlortest-durations.yml; andautobot-infrastructure/shared/tests/pytest.inisetstestpaths = unit integration e2e, which does not reachshared/scripts/. So all 18 suites were collected by nothing, and the fail-closed proof forgit worktree listwould have been verified exactly once — by hand, by me, in the PR claiming to restore it. That is the same shape this PR closes twice elsewhere, sitting inside the PR. Wired into all four places; mutation J below shows the pre-PR invocation is green on a broken fail-closed path and the wired one is red.One more defect surfaced while deriving the rule.
slm-post-commitis100755but carries its#!/bin/bashon line 4, below the copyright header, where it is a comment.execverefuses a file with no shebang and git falls back tosh, so a bash-only hook changed meaning silently. Shebang moved to line 1, and the guard now asserts the rule in both directions.What Changed
Index modes
Set with
git update-index --chmod=+x— a plainchmoddoes not reach the index undercore.fileMode=false.tools/git-hooks/pre-push100644100755tools/git-hooks/pre-commit100644100755tools/git-hooks/install_hooks.sh100644100755scripts/install-git-hooks.sh100644100755autobot-infrastructure/shared/scripts/hooks/function_length_checker.py100644100755autobot-infrastructure/shared/scripts/hooks/slm-post-commit100755100755(unchanged — shebang moved to line 1)tools/git-hooks/README.md100644100644(unchanged — not executable)Verified with
git ls-tree -r origin/Dev_new_guiagainstgit ls-files -son this branch.#14909 — the hooks
repo_tests/git_hooks_executable_test.py— reads the index mode viagit ls-files -sand asserts a derived rule in both directions: a file declaring a shebang must be100755, and a file that is100755must declare a shebang. Four reach floors, including an explicit extensionless count.repo_tests/git_hooks_installer_test.py— drives the real installer from inside a real linked worktree, deletes the worktree, and checks the hook is still a file, still executable and still byte-identical to its template. A second case starts from the state the repo was actually found in — a dangling symlink at the destination — and checks a re-run replaces it. git-hook enforcement silently dead: dangling pre-push symlink into deleted worktree; no pre-commit installed under explicit core.hooksPath #11598 made the installer copy-based, but nothing had ever asserted the property.tools/git-hooks/README.md— the "the hooks are symlinked, so edits are picked up on the next push" section has been wrong since git-hook enforcement silently dead: dangling pre-push symlink into deleted worktree; no pre-commit installed under explicit core.hooksPath #11598; it now says to re-run the installer and documents theupdate-indexspelling..github/filters/python-paths.yml— found while watching this PR's own CI.python-suiteis gated by that filter, and everything the three new guards assert on is a shell script or a file mode: no Python at all. A PR flippingpre-pushback to100644, or putting|| trueback into an extensionless hook, would have matched no pattern — the shim computespython != 'true', publishes the requiredpython-suitecontext green, and the guard written to catch exactly that never runs. The guards would have been inert on the only diffs they exist for. This is the shape that file's own header already documents for tech-debt(infrastructure): 18 sys.path bootstraps open-code the project root and default to the live install instead of the checkout #14544/fix(infrastructure): 18 sys.path bootstraps default to the live install instead of the checkout (#14544) #14575, so the fix is the same: name the inputs.test_the_python_filter_reaches_the_inputs_these_guards_readevaluates the globs against real paths rather than looking for the strings, because an entry that is present but no longer matches is worth nothing.#14891 — the lib-source guard
deliberately-exit-0category, asserted on behaviour and bounded above.post-commit-doc-sync:16— sourced with no failure block at all; it failed closed only because of the incidentalset -eat the top of the file. Now|| { … exit 1; }.pre-commit-warn-untracked:49— the decisive attempt wasif ! source … 2>/dev/null, discarding the library's own stderr, so a developer was told the load failed but never why. Redirect removed; theexit 0stays and is now declared.#14884 — the hook suites
pre-commit-worktree-branch-guard_test.py—subprocess.run(["command", "-v", "git"])→shutil.which("git").commandis a shell builtin; subprocess given a list never involves a shell. The old call's result was never even read.pre-commit-no-tag-pinned-action_test.py— the file path now goes in repo-relative withcwdat its root, the only shape pre-commit or a CI wrapper produces.repo_tests/hook_suite_harness_test.py— answers the issue's last acceptance criterion by AST, not grep, so a builtin named in a docstring is not a false positive.pytest.initestpaths+ all three CI invocations (ci.yml,coverage.yml,test-durations.yml) now nameautobot-infrastructure/shared/scripts/hooks. Measured before wiring, so this is a wiring fix and not a quarantine: 159 collected, 159 passed under this repo's ownpytest.ini(importlib import mode, strict markers). No tech-debt(tests): cross-backend + conftest-stub namespace pollution causes ~92% of full-suite collection errors and ~1400+ runtime failures (prereq for #10691) #13084-style package-name collision — the suites import only stdlib plus pytest, carry no__init__.py, and need no separate interpreter invocation. Sharding was checked too:stable_shardbuckets every collected module by path hash, so a module absent from.test_durationsis still assigned to a shard rather than silently dropped.autobot-infrastructureas a whole does not collect: 23 collection errors, andshared/scripts/test_alertmanager.pycallssys.exit(0)at import, which takes pytest down with anINTERNALERRORrather than a test failure. Filed as bug(ci): 56 of 74 test files under autobot-infrastructure are collected by nothing, and one kills the pytest session #14917 (56 of 74 infra test files collected by nothing, plus a secondtools/scripts/pipeline-scriptsdrift between the same three invocations) rather than bundled here.repo_tests/hook_suites_run_in_ci_test.py— guards both halves, because either alone passes while the wiring is broken. Configuration: the invocation set is derived by scanning the CI tree — all three pytest spellings, both YAML extensions, workflows and composite actions — never from a list of filenames. Measured: 64 invocations across 61 files, of which exactly 4 namerepo_testsas a bare path token (ci.yml,coverage.yml,marker-tests.yml,test-durations.yml);-p repo_tests.stable_shardis correctly not one of them. Carrier floor sits AT 4; the invocation floor at 40 catches a scanner that has stopped matching rather than letting it report an empty offender list. Behaviour: it runs the real collector under this repo'spytest.ini, because a path listed in a config that then ignores it collects nothing and exits zero.ci.yml,coverage.yml,test-durations.yml.marker-tests.ymlis the fourth carrier and is correctly not wired: it selects only marked tests, and the hook suites carry none of those markers.-mexpressionci.ymlnot integration and not slow and …coverage.ymlnot integration and not slow and …test-durations.ymlnot integration and not slow and …marker-tests.ymlintegration or slow or …python-paths.ymlnow saysautobot-infrastructure/**rather than two shell-shaped patterns (review's structural note).shell_lib_sources_resolve_test.pyfinds subjects by shebang, and "has a shebang naming a shell" cannot be written as a glob — a new extensionless script elsewhere under that tree would have matched neither pattern and taken the shim's green. The filter test's subjects are now derived fromgit ls-filesover the roots the guards read (646 non-Python files) instead of three hand-picked paths.Verification
Every number below was measured, not estimated. Guards and mutations were run against a scratch copy of the tree outside the repository; nothing was mutated in the worktree and no mutation is in any commit.
Measured reach — guarded / total
100755The
+16distinct scripts are exactly the 16 extensionless hooks #14891 enumerated.Suites, before and after
pre-commit-no-tag-pinned-action_test.py+pre-commit-worktree-branch-guard_test.py, on a clean copy of base: 8 failed, 10 passed, reproducing the issue exactly. After the harness fix: 18 passed.The five new/changed
repo_testsmodules plus the widened guard: 93 passed. With the newly-wired hook suites included: 252 passed.Local interpreter: CPython 3.10.12, pytest 9.0.2. Counts are interpreter-specific; CI is the authority.
Mutation — every guard was broken on purpose and confirmed red
git update-index --chmod=-x tools/git-hooks/pre-pushtest_every_shebanged_hook_is_tracked_executable,test_the_three_hooks_that_were_inert_are_executableslm-post-commit's shebang back below the headertest_no_executable_hook_relies_on_a_missing_shebang|| truein an extensionless hook (pre-commit-branch-guard:30)2>/dev/nullonwarn-untracked's decisive attempttest_a_missing_lib_is_never_swallowedlib-source-contractmarker (bareexit 0)rglob("*.sh")["command", "-v", "git"]test_no_hook_suite_spawns_a_shell_builtingit worktree listfailuretest_a_git_worktree_list_failure_does_not_report_cleanpython-paths.ymltools/git-hook/**)ci.yml's invocationtest_every_ci_pytest_invocation_that_runs_repo_tests_also_runs_the_hookspytest.initestpathstestpathsinside a comment; now parsed withconfigparserpre-commit-worktree-branch-guardrepo_testsonly): 93 passed, green — the regression ships. Wired invocation (repo_tests+ the hooks path, whatci.ymlnow runs): 1 failed, preciselytest_a_git_worktree_list_failure_does_not_report_clean. Reverted: 252 passed.python-paths.ymlback to the shell-shaped patternsmarker-tests.yml53f4d306fpython -m pytestonlyonly found 19 pytest invocations— the guard cannot silently shrink its own reachci.ymlunder the new marker-aware ruleci.ymlexactly@pytest.mark.integrationpre-commit-branch-guard_test.py:48; the exemption is earned, not asserted$MARKER_EXPRESSIONunresolvablemarker-tests.ymlto the exclusion expressionEach mutation was reverted and the suite returned to green before the next.
Not verified here
The exec-bit fix is a tracked-mode change; whether a given developer's
.git/hooks/is repaired depends on them re-runningbash scripts/install-git-hooks.sh. That is per-clone state this PR cannot reach, and the README now says so.Residual filed
#14920 — 262 tests across the repo
returna value instead of asserting, so they pass whichever branch they take (backend_startup_e2e_test.py::test_importsreturns a bool). Surfaced by aPytestReturnNotNoneWarningin this PR's own CI log. Counted by AST, attributing a return only to the test that directly encloses it — a naive walk inflates it to 739. Zero of them are in the directory this PR wires into CI, so this branch neither introduces nor inherits any.#14917 — 56 of the 74 test files under
autobot-infrastructure/are still collected by nothing,shared/scripts/test_alertmanager.pykills a pytest session withINTERNALERRORat import, andcoverage.yml/test-durations.ymlomit thetools/scripts/pipeline-scriptsroots thatci.ymlcarries. Not bundled here: it needs 23 collection errors triaged first, which is a different size and risk of change. Cross-linked both ways.Note the reviewer's "all three pass the same explicit path list" is very slightly off —
ci.ymlcarries three roots the other two do not. The hooks omission was identical in all three and is fixed in all three; the other drift is #14917.Model Used
Claude Opus 5 (1M context) —
claude-opus-5[1m]