Skip to content

fix(hooks): hooks that never run, and the guards that cannot see them (#14909, #14891, #14884) - #14916

Merged
mrveiss merged 9 commits into
Dev_new_guifrom
issue-14909-hook-exec-bits
Aug 24, 2026
Merged

mrveiss merged 9 commits into
Dev_new_guifrom
issue-14909-hook-exec-bits

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 23, 2026 •

Copy link
Copy Markdown
Owner

Closes #14909, #14891, #14884

Three findings, one root cause: hook files are tracked without the executable bit, and core.fileMode=false hides 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 *.sh glob, a FileNotFoundError — but they are one shape: a check that is absent reads exactly like a check that passed.

core.fileMode=false is the mechanism behind the first. A plain chmod +x makes the working file executable and never reaches the index; git status stays clean, the local hook runs, and every other clone gets 100644 — which git skips silently. So the fix has to be git update-index --chmod=+x, and the guard has to read git ls-files -s. A guard using os.access or stat would 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_exits requires 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. 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 — a lib-source-contract: non-blocking marker, a message on stderr, an explicit exit 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) >= 60 and >= 55 distinct scripts were all satisfied by the .sh population 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 to rglob("*.sh") fails on something the .sh files 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 are AssertionError: 0 == 1, from a different cause: the harness passed an absolute /tmp/pytest-.../ path in argv, #13936 made get_staged_files apply 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.py derived its invocation set by scanning the workflows — and immediately went red on marker-tests.yml, a fourth invocation neither the review nor I had listed. Then the failure exposed that the sweep was only half-derived: it matched python -m pytest and nothing else, while the CI tree actually contains 27 bare pytest, 19 python -m pytest and 1 python3 -m pytest. It reached marker-tests.yml only 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, .yml and .yaml, and .github/actions/ composite actions as well as .github/workflows/.

Then wiring marker-tests.yml turned out to be the wrong fix, and CI said so — 20 failed / 19 errors, every one fixture 'backend_url' not found in 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 carries pull_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 -m expression. $MARKER_EXPRESSION is 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.yml stops being exempt. marker-tests.yml is restored byte-identical to base (blob 6281b27ca), 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's testpaths listed 8 roots and autobot-infrastructure was in none of them; neither was it in ci.yml, coverage.yml or test-durations.yml; and autobot-infrastructure/shared/tests/pytest.ini sets testpaths = unit integration e2e, which does not reach shared/scripts/. So all 18 suites were collected by nothing, and the fail-closed proof for git worktree list would 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-commit is 100755 but carries its #!/bin/bash on line 4, below the copyright header, where it is a comment. execve refuses a file with no shebang and git falls back to sh, 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 plain chmod does not reach the index under core.fileMode=false.

File Before After
tools/git-hooks/pre-push 100644 100755
tools/git-hooks/pre-commit 100644 100755
tools/git-hooks/install_hooks.sh 100644 100755
scripts/install-git-hooks.sh 100644 100755
autobot-infrastructure/shared/scripts/hooks/function_length_checker.py 100644 100755
autobot-infrastructure/shared/scripts/hooks/slm-post-commit 100755 100755 (unchanged — shebang moved to line 1)
tools/git-hooks/README.md 100644 100644 (unchanged — not executable)

Verified with git ls-tree -r origin/Dev_new_gui against git ls-files -s on this branch.

#14909 — the hooks

#14891 — the lib-source guard

  • Shell detection by shebang, not suffix or a name list.
  • The deliberately-exit-0 category, asserted on behaviour and bounded above.
  • All floors re-derived; a new extensionless floor and a named extensionless call site.
  • 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. Now || { … exit 1; }.
  • pre-commit-warn-untracked:49 — the decisive attempt was if ! source … 2>/dev/null, discarding the library's own stderr, so a developer was told the load failed but never why. Redirect removed; the exit 0 stays and is now declared.

#14884 — the hook suites

  • pre-commit-worktree-branch-guard_test.py — subprocess.run(["command", "-v", "git"]) → shutil.which("git"). command is 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 with cwd at 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.
  • The suites are now actually collected. pytest.ini testpaths + all three CI invocations (ci.yml, coverage.yml, test-durations.yml) now name autobot-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 own pytest.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_shard buckets every collected module by path hash, so a module absent from .test_durations is still assigned to a shard rather than silently dropped.
  • 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, which takes pytest down with an INTERNALERROR rather 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 second tools/scripts/pipeline-scripts drift 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 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. 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's pytest.ini, because a path listed in a config that then ignores it collects nothing and exits zero.
  • Three invocations are wired — ci.yml, coverage.yml, test-durations.yml. marker-tests.yml is the fourth carrier and is correctly not wired: it selects only marked tests, and the hook suites carry none of those markers.
Carrier -m expression Selects unmarked? Hooks path
ci.yml not integration and not slow and … yes wired
coverage.yml not integration and not slow and … yes wired
test-durations.yml not integration and not slow and … yes wired
marker-tests.yml integration or slow or … no exempt, and the exemption is re-derived
  • python-paths.yml now says autobot-infrastructure/** rather than two shell-shaped patterns (review's structural note). shell_lib_sources_resolve_test.py finds 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 from git ls-files over 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

Guard Before After
lib-source: distinct scripts with a lib site 55 / 71 71 / 71
lib-source: source sites 61 / 78 78 / 78
lib-source: shell scripts walked 160 180
lib-source: decisive sites classified 56 73
exec bit: shebanged files at 100755 20 / 25 25 / 25
exec bit: extensionless files covered 0 22
harness: hook suites parsed / spawn sites — 18 modules, 67 sites, 0 offenders

The +16 distinct 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_tests modules 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

# Mutation Result
M1 git update-index --chmod=-x tools/git-hooks/pre-push 2 failed — test_every_shebanged_hook_is_tracked_executable, test_the_three_hooks_that_were_inert_are_executable
M2 push slm-post-commit's shebang back below the header 2 failed — incl. test_no_executable_hook_relies_on_a_missing_shebang
M3 reintroduce || true in an extensionless hook (pre-commit-branch-guard:30) 2 failed — the #14891 acceptance mutation
M4 restore 2>/dev/null on warn-untracked's decisive attempt 1 failed — test_a_missing_lib_is_never_swallowed
M5 drop the lib-source-contract marker (bare exit 0) 3 failed — the category is not free
M6 narrow the walk back to rglob("*.sh") 5 failed — the widening is load-bearing
M7 reintroduce ["command", "-v", "git"] 1 failed — test_no_hook_suite_spawns_a_shell_builtin
A hook stops rejecting third-party tag pins 7 failed — the supply-chain property is live again
B hook stops allowing first-party actions 5 failed — proves the 3 "allow" tests are no longer vacuous
C worktree guard swallows a git worktree list failure 1 failed — test_a_git_worktree_list_failure_does_not_report_clean
D installer symlinks instead of copying 2 failed
E installer becomes a no-op 2 failed — absent does not read as clean
F drop the hook entries from python-paths.yml 1 failed — the guards would not have gated their own subject
G keep the entries but misspell one (tools/git-hook/**) 1 failed — the check evaluates the glob, it does not grep for it
H drop the hooks path from ci.yml's invocation 1 failed — test_every_ci_pytest_invocation_that_runs_repo_tests_also_runs_the_hooks
I drop the hooks path from pytest.ini testpaths 1 failed — and it exposed a sloppy parser in my own test, which split on the first testpaths inside a comment; now parsed with configparser
J break the fail-closed path in pre-commit-worktree-branch-guard pre-PR invocation (repo_tests only): 93 passed, green — the regression ships. Wired invocation (repo_tests + the hooks path, what ci.yml now runs): 1 failed, precisely test_a_git_worktree_list_failure_does_not_report_clean. Reverted: 252 passed.
K narrow python-paths.yml back to the shell-shaped patterns 1 failed — the derived subject list catches what three hand-picked paths did not
L un-wire marker-tests.yml 1 failed, naming it exactly — this is the mutation CI performed for real on 53f4d306f
M narrow the invocation regex back to python -m pytest only 1 failed on only found 19 pytest invocations — the guard cannot silently shrink its own reach
N un-wire ci.yml under the new marker-aware rule 1 failed, naming ci.yml exactly
O give a hook test @pytest.mark.integration 1 failed — the exemption dies and names pre-commit-branch-guard_test.py:48; the exemption is earned, not asserted
P make $MARKER_EXPRESSION unresolvable 1 failed — "unknown is not an exemption", rather than silently exempting
Q switch marker-tests.yml to the exclusion expression 1 failed — the marker-only branch would be untested, so the classification cannot quietly collapse to one case

Each 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-running bash 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 return a value instead of asserting, so they pass whichever branch they take (backend_startup_e2e_test.py::test_imports returns a bool). Surfaced by a PytestReturnNotNoneWarning in 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.py kills a pytest session with INTERNALERROR at import, and coverage.yml/test-durations.yml omit the tools/scripts/pipeline-scripts roots that ci.yml carries. 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.yml carries 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]

…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).
@github-actions

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant