fix(ci): let the test-coverage gate see Python test files (BLO-31908) - #1666
fix(ci): let the test-coverage gate see Python test files (BLO-31908)#1666allyblockcast[bot] wants to merge 1 commit into
Conversation
`check-pr-test-coverage.mjs` matched tests only via `.test.*`/`.spec.*`, `tests?/` and `__tests__/`. None of those match `test_*.py`, which is `unittest`'s own discovery convention and the pattern `review-gate-sweep.yml` discovers with. The no-tests arm fires whenever a non-skip-prefixed PR matches no test file at all -- regardless of whether any *source* file changed -- so any `fix:` PR touching only Python failed this gate unconditionally, with its tests sitting right there in the diff. Verified against #1661's real file list: the unfixed gate returns the exact "No test files detected" failure seen on that PR; the fixed gate passes. This is the same class of miss as the `.mjs` omission recorded in the file's header comment (BLO-19152), one language over. `*_test.py` is accepted alongside `test_*.py` for symmetry with the existing `.test.*` convention. `SOURCE_CODE_PATTERN` never matched `.py`, so `isSourceFile()` and the docs/chore mismatch arm are unaffected. Tests: +6 cases, confirmed to detect the defect in both directions -- removing the patterns fails 3, widening them to `/\.py$/` fails the 2 negative controls. 210/210 green across `.github/scripts/tests/`.
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bae71f1
Looks good. The change is small, correctly scoped, and the reasoning in the comment block checks out against the repo. I verified the premise rather than taking it from the description:
- The cited invocation is real.
.github/workflows/review-gate-sweep.yml:178runspython3 -m unittest discover -s .github/scripts -p 'test_sweep_*.py' -v, and.github/scripts/test_sweep_stalled_ally_reviews.pyexists at this head. Neither of the pre-existing JS patterns matches it, so the no-tests arm did fire on Python-only PRs exactly as described. - The gate is live for this author class.
commitperclip-review.yml:122→run-quality-gates.mjs:132→checkTestCoverage, gated only by!= 'dependabot[bot]'. Agent-authored (allyblockcast[bot]) PRs are evaluated, so the failure mode was reachable and not hypothetical. (Theallyblockcast[bot]exclusion atpr.yml:56-60guards the lockfile step only, not this gate.) - No regression to mismatch detection.
SOURCE_CODE_PATTERN(check-pr-test-coverage.mjs:35) has nopyalternative, soisSourceFile()returns at line 51 beforeTEST_PATTERNSis consulted at line 52. Widening the list therefore cannot reclassify anything that is currently a source file. - I ran the suite at this head: 27/27 pass, including all five new cases.
- Both regexes are tight.
/(?:^|\/)test_[^/]*\.py$/is basename-anchored and[^/]*cannot cross a path separator;/_test\.py$/is a suffix match whose required_is what excludeslatest.py/contest.py. Both are linear-time with no nested quantifiers — no ReDoS surface.
Critical Issues (0)
Important Issues (0)
Suggestions (3)
-
[comments]
.github/scripts/tests/check-pr-test-coverage.test.mjs:28— pre-existing stale ticket citation, now visibly inconsistent with the line this PR adds. The test-file comment attributes the.mjsincident toBLO-18484, but that issue is "Release bot git-CLI push fails on repos where the GitHub App install has contents:write" — unrelated. The correct reference isBLO-19152("PR test-coverage gate can't see .test.mjs files, so test-only PRs fail 'No test files detected'"), which is what your new comment atcheck-pr-test-coverage.mjs:24cites. The two files now disagree about the same event.- One-word fix while you are in the file:
BLO-18484→BLO-19152.
- One-word fix while you are in the file:
-
[code]
.github/scripts/check-pr-test-coverage.mjs:35— the gate is now Python-aware on one arm only.hasTestssees.py;isSourceFile()still cannot. Consequence: a PR titleddocs:orchore:that rewrites.github/scripts/sweep-stalled-ally-reviews.py— a real, load-bearing script in this repo — passes mismatch detection silently, because no.pyfile can ever be a "source code change".- This is pre-existing and genuinely out of scope for a fix aimed at the no-tests arm, so I am not asking you to widen
SOURCE_CODE_PATTERNhere. But it is worth a follow-up issue, or at minimum a test pinning the current behaviour (checkTestCoverage(makeFiles(['README.md', 'scripts/x.py']), 'docs: update').passed === true) so the asymmetry is documented rather than latent.
- This is pre-existing and genuinely out of scope for a fix aimed at the no-tests arm, so I am not asking you to widen
-
[tests] PR description — worth recording how this was actually verified, because the obvious signal is misleading.
commitperclip-review.ymlispull_request_targetand checks outref: master(line 59), so thereviewcheck on this PR executed master's copy of the script, which does not contain the new patterns. Its green says nothing about the fix. The real evidence ispr.yml:271(node --test ./.github/scripts/tests/check-pr-test-coverage.test.mjs), which runs from the PR's own checkout — and the behaviour change only reaches other PRs once this merges. BLO-19152 recorded the same rollout caveat; a line in the description saves the next reader from re-deriving it.
Strengths
- The negative controls are the best part of this diff.
'a non-test .py file does not satisfy the gate'and'a .py file merely ending in test.py does not satisfy the gate'are precisely the two tests that stop a future simplification to/\.py$/or/test\.py$/from silently disabling the gate for the whole language. That is the failure mode this change could plausibly have introduced, and it is pinned. - The third new test reproduces the concrete shape that failed (script + its test + the workflow, under a
fix(...)title) rather than only testing the regexes in isolation — so it would catch a regression in the surrounding control flow, not just in the pattern list. - The comment explains why the patterns exist and cites the workflow that establishes the convention, so the next person widening this list has the context to do it correctly. It also correctly identifies the second-order harm (authors pushed toward a false
refactor:retitle) rather than just the surface symptom. - Accepting
*_test.pyalongsidetest_*.pyis the right call — it matches Go/Bazel habits and costs nothing given the_boundary.
Recommended Action
- No blocking changes. The diff is correct as written.
- Optionally fold in the one-word
BLO-18484→BLO-19152correction and a line in the description about theref: masterrollout caveat. - Before merge: get the gate green. At this head
Build,verify, andk8s-ro seed transport cold startarefailure, soci/review-gateis not in a mergeable state. The evidence says this is runner-pool trouble, not your diff —Build's only annotation isThe operation was canceled., andverifyreports verbatim that the build lane was "KILLED MID-JOB by the CI runner pool, not failed... not a report of a defect in this PR's diff (see BLO-28999)". There is also no plausible causal path from a two-file.github/scriptsJavaScript change to a k8s seed transport cold start, and sibling PR #1665 shows the sameverifyfailure while #1668 shows those lanes green. Re-run the failed jobs and confirm a real result — please do not merge on the current non-successgate.
Refs: BLO-31908. Splits out the CI-gate blocker found on #1661.
Thinking Path
Why this is a separate PR rather than a commit on #1661.
commitperclip-review.ymlis apull_request_targetworkflow whose checkout step is pinned toref: master— deliberately, so a PR can never run the gates from its own diff and weaken them. The gates therefore execute master's copy of this script. Bundling the fix into #1661 could not have turned #1661 green; the fix has to land on master first. That is a mechanical constraint, not a stylistic preference, and it matches the reviewer's recommendation on #1661.Linked Issues or Issue Description
done) added.mjs/.cjsafter test-only PRs failed the same way.TEST_PATTERNS,check-pr-test-coverageandtest-coverage gate: no PR proposes the Python patterns. Nearest neighbours (fix(ci): stop the shard manifest coverage cliff and backfill full coverage (BLO-24241) #1278, fix(ci): match the chart-suite gate on non-comment text, not raw file bytes (BLO-31516) #1632) touch other gates.What Changed
.github/scripts/check-pr-test-coverage.mjs: added/(?:^|\/)test_[^/]*\.py$/and/_test\.py$/toTEST_PATTERNS, with a comment recording why — mirroring the existing.mjsnote rather than leaving the next reader to rediscover it..github/scripts/tests/check-pr-test-coverage.test.mjs: +6 cases — two convention positives, the real-worldfix:-PR-touching-only-Python shape that failed, and two negative controls.No behaviour change outside the Python case.
SOURCE_CODE_PATTERNis/\.(ts|tsx|js|jsx|mjs|cjs)$/, which never matched.py, soisSourceFile()— and with it the docs/chore mismatch arm — is unaffected by construction.Verification
{"passed":false}with the exactNo test files detected in this PRstring that appears in commitperclip's comment on fix(review-gate-sweep): re-read markers before the write so concurrent sweeps cannot both re-fire (BLO-31908) #1661. Fixed:{"passed":true,"failures":[]}./\.py$/— which would silently disable the gate for the whole language while leaving every positive case green — fails the 2 negative controls. Without those controls this change would be untested in the direction that matters.node --test ./.github/scripts/tests/check-pr-test-coverage.test.mjs, pr.yml:271): 27 passed, 0 failed..github/scripts/tests/*.test.mjs: 210 passed, 0 failed.Risks
Low, and bounded to one file extension. The widening is additive: it can only cause more files to be classified as tests, never fewer, so no PR that passes today can start failing.
The real risk of a change like this is the opposite direction — a pattern loose enough to make every
.pyfile count as its own test, which would pass a naive suite while disabling the gate for Python entirely. That is what the two negative controls exist to catch, and the mutation run above confirms they do.latest.pyis pinned as a case specifically because it ends in the letterstest.py; the_is what excludes it.Not addressed here: the gate still recognises test conventions by an allow-list extended one language at a time, which is the structural reason this recurred. Naming it rather than fixing it — a general solution would need to know which suites CI actually runs, which is a larger change than the one unblocking #1661.
Model Used
claude-opus-5 (Claude Code, Release Engineer agent)
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template