perf(prepare-pr): skip the test suite a change cannot affect, using CI's own selector - #5262
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Coarse, fail-closed, CI-parity reduction with honest scoping — the unsound within-surface narrowing was removed rather than patched, which is the right call. Suggestions
[DESIGN-REVIEWED] 0f99744 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe candidate list contains no candidates, and my independent trace of No findings. [OPUS-REVIEWED] 0f99744 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
8008ac2 to
82943be
Compare
|
Addressed on GPT BLOCKING -- "scoped selector skips configured test roots" -- FIXED, and it was worse than reported.
Proof the fix has an effect, not just a passing test: a change to Chasing that finding surfaced two more real defects in my own code, both now pinned by self-test assertions:
Design CONCERNS #1 -- "undocumented happy-dom revert" -- was a stale-base artifact, now gone. The branch was cut at Design CONCERNS #2 -- "the invariant holds only for zero-reference files" -- correct, and the docs were overclaiming. Docs fixed rather than the claim defended. You are right that the trigger is zero references, not incomplete ones: when some test names a changed module the run narrows to those, so a test that breaks through a transitive import (test -> consumer -> changed module, never spelling the changed module) is not selected. I did not widen the escalation rule to cover it, because "escalate whenever references might be incomplete" is unfalsifiable from a text scan and collapses back into always running everything, which is the gate this replaces. Verification on this revision: scoped self-test green (now covering all six escalation paths, the roots fix, the boundary fix, the role-vs-location fix and the deletion rule); |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of Verification done. Findings grounded; writing the review. First-Principles-Verdict: CONCERNS
What this change shipsIntent: stop paying the full 62k-test local suite on every prepare-pr iteration when the diff cannot affect it — a FIX for a measured cost.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 0f99744 |
82943be to
1ec749d
Compare
|
Both blocking findings fixed on Root cause of this round AND the last one: the selector was a denylist. It asked "is this path one of the doubts I know about?" and silently skipped everything else. That is why each round surfaced a new spelling -- round 1 a second configured testpath, round 2 a The question is now inverted into an allowlist: "is this a path I can positively prove I know how to map?" Four verdicts, and
Per finding:
A third defect your finding exposed, which you did not name and which was the more dangerous one: One thing I tried, measured, and reverted, recorded so it is not re-proposed. The obvious way to keep data files narrow is to match them by BASENAME. It is unsound: scoping on Verification on this revision: scoped self-test green with new assertions pinning every classifier verdict, the in-package-helper rule, the css rule, the ALWAYS_ON masking bug and the bare-basename trap; On the SAST (Semgrep) red: its own log shows the failure is in the |
1ec749d to
c32092d
Compare
|
Both blocking findings fixed on "Packaged text changes can bypass their contract tests" -- correct, and this PR is its own counterexample. The inert rule keyed on the extension ( Inertness is now a narrow path allowlist ( "Rename handling loses the deleted source path" -- correct, and it defeated a fallback that already existed. Keeping only the Verified with a real rename rather than an assertion alone: Two mistakes of my own from this round, recorded because one of them is in this repo's own gate rules. The
Standing note on the shape of this review, since this is round 3 of the same class. Rounds 1-3 were all one root cause -- the classifier enumerated the doubts it knew about and skipped the rest -- and round 2 inverted it into an allowlist for exactly that reason. Rounds 1 and 2 were genuine holes in the old denylist. This round's two are different in kind: both are inside the allowlist, in the two places where a positive claim was too generous (an extension standing in for a path, a rename standing in for one path instead of two). That is the expected convergence shape rather than a new family, and each is now pinned by an assertion, but if a round 4 lands another "the positive claim was too generous" finding I would rather split the remaining tightening into a follow-up than keep growing this diff. |
|
Correction: I was wrong about the Semgrep red, twice. I called it infra in two earlier comments on the basis of a The finding: It is not command injection: argv is always a list and Fix: every target now passes One thing that fix broke, caught by running it rather than trusting it. I also added a Re-verified after the fix: frontend scoped run back to 4 files, backend scoped run 22 files / 1012 tests with One base-owned red worth flagging, not mine: Gates on this revision, each with its exit code captured directly rather than through a pipe: self-test, profiles suite (33 passed), black, isort, flake8, docs-lint, scrub-lint, brand-name -- all rc=0. |
c32092d to
710ca71
Compare
|
All three round-6 findings are real. I verified each against the tree rather than accepting the description, and the numbers are worth stating because they are the argument:
I am pausing rather than fixing these, because the pattern is now a design verdict rather than a defect list. Six rounds, nine findings, zero false positives -- and every one is the same impossibility: a text reference scan cannot enumerate the ways a test can reach a module. The spellings found so far are absolute import, relative import, barrel re-export, in-package fixture, global vitest setup, data-file read, cross-surface parity comparison, and documentation contract. There is no reason to believe that list is finished. Each suggested remedy also shrinks the allowlist -- "or fall back to the full suite", "escalate What in this PR is sound independently of reference scanning, and worth keeping:
What is not sound and generated all nine findings: the per-file reference scan that narrows within a surface. Making that sound needs a real import graph -- Python AST plus a TS resolver that follows barrel re-exports -- which is a different and much larger piece of work than this PR. So the proposal is to reduce this PR to the sound subset (surface split + escalation + hardening, dropping the within-surface reference narrowing), and file the import-graph selector as a follow-up. That keeps a real, measured win for the common single-surface PR with no soundness argument left, and it costs the reduction on mixed-surface diffs. Holding for a maintainer decision before I touch it further. |
d229c52 to
5831a31
Compare
5831a31 to
1fede7f
Compare
|
Fixed on "CI meta paths incorrectly reduce the frontend suite" -- correct, and broader than the two paths named. I verified rather than assumed: Rather than broaden two prefixes by hand, I transcribed That also settles the Verified by probe on all four combinations: a On the Semgrep red, which was real again and was my own regression. When I rewrote the runner I split the Gates on this revision, each exit code captured by redirecting to a file and testing |
1fede7f to
3686284
Compare
|
All three review lanes went clean on "Git C-quotes hostile filenames" -- correct. Without Both git reads are now NUL-delimited ( Behaviour re-verified unchanged after the rewrite: a Gates on this revision, each exit code captured by redirecting to a file and testing Note for the lanes: Opus's comment is still stamped |
…o the full suite The floor's two test gates were `python -m pytest -q` (62,108 collected tests; collection alone ~100s) and `npm --prefix website test` (~1,444 spec files). The loop runs its gate up to ten times per PR and CI runs the full suite on refs/pull/<N>/merge regardless, so the local full run bought a signal CI already produces. Replaces both with scripts/run_scoped_tests.py, which runs the tests that reference what the diff touched and escalates to the FULL suite on every path where narrowing cannot be justified -- mirroring the rule ci.yml's own "Select test scope" steps state. This is deliberately NOT "run only the new tests": a change's own new tests say nothing about the existing tests it breaks. jscpd and test:electron become explicit floor entries, because `npm test` ran them transitively via pretest and the scoped gate replaces only the vitest part.
3686284 to
0f99744
Compare
|
Fixed on "Native Windows cannot launch the npm command shims" -- correct, and it is specific to this PR rather than inherited. The floor's other npm entries ( I did not take the suggested remedy literally. Hardcoding Verified end to end rather than by assertion alone: a backend-only diff drove the frontend gate through the resolved launcher and the cross-surface run completed green -- Gates on this revision, each exit code captured by redirecting to a file and testing Opus is still stamped |
1. What is the problem?
The
prepare-prgate floor ran both test suites in full, locally, on every iteration of its inner loop:python -m pytest -q-- 62,108 collected tests. Collection alone takes ~100s before a single test executes.npm --prefix website test-- ~1,444 frontend spec files, plus jscpd and the Electron specs viapretest.Phase 2 is a bounded loop capped at 10 rounds, and CI runs the full suite on
refs/pull/<N>/mergeregardless of what ran locally. Most PRs touch one surface, so half of that work could not tell the author anything -- a frontend-only PR was running all 62k backend tests to learn nothing.2. Why this issue matters to the user
It is the largest cost in the loop and it is paid on a workstation rather than on eight sharded CI runners. Every review round pays minutes to tens of minutes of wall clock before any reviewer runs. On a busy machine the aggregate agent memory crosses the cgroup
MemoryHighceiling and the kernel throttles the whole agent subtree, which surfaces as unrelated 30sinitializetimeouts in other sessions.3. How our fix solves it
scripts/run_scoped_tests.pyperforms exactly one reduction, and it is CI's own: when a diff touches only one surface, the other surface runs the cross-surface set instead of its full suite. Measured on this checkout, 350 backend files for a frontend-only diff and 146 frontend specs for a backend-only one, viascripts/ci-surface-tests.py-- the same script and the same post-processingci.ymluses, so there is no second selector here that can silently disagree with CI. A plain skip would have been unsafe: a frontend-only change really can break a backend test that reads a frontend module.Four verdicts, and everything that is not a reduction runs everything:
What this deliberately does not do, and why that is the headline of this PR. It does not narrow within the surface a change touches. That was implemented here first, by scanning tests for textual references to the changed module, and it was removed after six review rounds produced nine findings -- all real, all one impossibility: a text scan cannot enumerate the ways a test can reach a module. The spellings found were absolute import, relative import, barrel re-export, in-package fixture, global vitest setup, data-file read, cross-surface parity comparison, and documentation contract, and every remedy shrank the allowlist further toward "escalate everything", which is the full suite again. Doing it soundly needs a real import graph; that is now #5303.
The nine measured traps from the removed attempt are recorded in
references/gate-floor.mdrather than deleted, because #5303 will meet the same ones: bare-stem matching selecting 621 of ~700 test files, a barrel module whose 128 real consumers are invisible next to 235 incidentalindexmentions,vitest run -- <paths>silently running all 22,939 tests while reporting a narrow scope, and six more.Also in this PR, independent of any reduction:
--config=evil.iniwould reach pytest as an OPTION.validated_targets()requires a plain relative path resolving to a real file inside the runner's root;--is added for pytest and deliberately NOT for vitest.test:electronbecome explicit floor entries.npm testran them transitively viapretest; the cross-surface path runs only vitest, so without their own entries they would have vanished from the floor as a side effect of a speed change.website/src/test/setup) hid the real vitest setup graph from broad-impact classification for four review rounds, because a dead path is indistinguishable from a working one.4. What tests we did
SCOPED_TESTS_BASE_REFboth exit 2, never a silent reduction.--test, wired into the floor ahead of its scans): asserts every hardcoded broad-impact path resolves on disk, that documentation is backend-owned rather than inert, that the cross-surface list arrives in each runner's own path space, that hostile targets (--config=evil.ini,-p no:randomly,../outside.py,/etc/passwd, a non-existent file) cannot reach argv, and that the vitest argv carries no--.test/test_prepare_pr_profiles.py33 passed (was 30). The two new floor assertions were mutation-verified red-then-green in an earlier revision: strippingSCOPED_TESTS_BASE_REFreddens the base-ref test, deleting the jscpd lane reddens the dropped-lane test.Not run locally, stated plainly: the full 62k backend suite. This PR's own diff touches the runner, which is a broad-impact path, so the gate correctly escalates itself to the full suite -- and running that serially is the cost this PR exists to reduce. CI runs it on the merge ref. Separately,
test/test_acp_backend_kas.py::TestNoImportCyclefails on a clean checkout of this branch's base on this host (unshare(CLONE_NEWUSER)returns EPERM), unrelated to this diff.5. Any other suggestions on the work
gate-floor.mdalready documented an accepted asymmetry forcheck_per_file_coverage.py(its enforcement form needs a full coverage run, so it surfaces in CI rather than locally). The surface split is the same trade made explicit; it may be worth naming that pattern once rather than per-gate.setup[]andgates[](prepare-pr profile: split setup[] from gates[] so provisioning is not a verdict #2599) would letnpx playwright install chromiumstop masquerading as a gate. Unrelated to this PR but adjacent.Follow-up: #5303