fix(cli): scan seven more 3.x → 4.x breakers in wheels upgrade check#2645
Conversation
The `wheels upgrade check --to=4.0.0` 3.x → 4.x branch only flagged the legacy plugins/ directory, the wheels.Test base class, and application.wirebox references — 3 of the 11 documented breakers from the canonical upgrade guide. Users could run the check, see a short list, upgrade, and trip over the unscanned breakers in production; the tool's silence read as a green light. Extend the checks array with seven more grep- and directory-based scans: - CORS middleware without allowOrigins (deny-all default in 4.0) - RateLimiter middleware without explicit trustProxy / proxyStrategy - allowEnvironmentSwitchViaUrl=true (default flipped in production) - Missing csrfEncryptionKey in config/ (cookies rotate per deploy) - Legacy `wheels snippets` invocations in Makefile, package.json, .github/workflows/*.yml, and top-level *.sh scripts - Legacy tests/specs/functions/ directory rename - Vite asset helpers (viteStrictManifest default flip) The grep runner gains two backward-compatible options to support these checks: `scanTargets` (mixed list of files + directory roots for the multi-root snippets scan) and `absent` (warn when the pattern is NOT found anywhere, used by the csrfEncryptionKey check). Existing 2.x → 3.x checks and the three pre-existing 3.x → 4.x checks are unchanged. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…rence Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
|
There was a problem hiding this comment.
Wheels Bot — Reviewer A
This PR closes #2628 by extending runUpgradeCheck() with seven additional 3.x → 4.x breaker scans: CORS deny-all default, RateLimiter hardened defaults, allowEnvironmentSwitchViaUrl flip, missing csrfEncryptionKey, legacy wheels snippets CLI invocations, the tests/specs/functions/ rename, and the viteStrictManifest default flip. The implementation is solid — the absent/scanTargets runner extensions are logically correct, CHANGELOG and test coverage are included, and the commit conforms to commitlint. I have a latent cross-engine nit, one advisory design concern, and a test-coverage gap to note; none rises to request-changes.
Correctness
RateLimiter check flags every instantiation, including already-correct configs — by design, but worth calling out explicitly. cli/lucli/Module.cfc:3631:
pattern: "new\s+wheels\.middleware\.RateLimiter",Any user who upgraded incrementally and already set trustProxy / proxyStrategy explicitly will still see the warning on re-run. The fix text says "verify" (good), but the description says "verify trustProxy and proxyStrategy" — a user whose config already reads new wheels.middleware.RateLimiter(trustProxy=true, proxyStrategy="first") could reasonably interpret this as a scanner false alarm rather than a useful signal. Consider strengthening the description to something like "RateLimiter middleware — defaults changed in 4.0, review config" to make it clear the check is advisory regardless of current state.
CORS pattern misses multi-line no-arg instantiation (very narrow edge case). cli/lucli/Module.cfc:3619:
pattern: "new\s+wheels\.middleware\.Cors\s*\(\s*\)"\(\s*\) only matches when the open and close parens are on the same line. A rare but valid form:
new wheels.middleware.Cors(
)would not be caught. Acceptable for a best-effort static scan, but worth noting as a known gap.
Cross-engine
Latent Adobe CF hazard on the scanTargets extension fallback. cli/lucli/Module.cfc:3739:
var exts = structKeyExists(target, "extensions") ? target.extensions : (check.extensions ?: "");The Elvis ?: on check.extensions would crash Adobe CF with EXTENSIONS is undefined if a scanTargets entry uses a directory target that omits extensions and the parent check struct also lacks a top-level extensions key. In the current PR every directory target provides its own extensions key (lines 3669–3672), so the fallback is dead code today. But it's a maintenance trap — anyone adding a new directory target without extensions later will hit this on Adobe CF only.
Safer form (consistent with .ai/wheels/cross-engine-compatibility.md struct-access guidance):
var exts = structKeyExists(target, "extensions") ? target.extensions
: (structKeyExists(check, "extensions") ? check.extensions : "");Tests
The spec in vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc uses static source inspection of Module.cfc via fileRead + findNoCase. This is a pragmatic choice (no LuCLI runtime available in the test harness) and keeps the spec lightweight. The trade-off is that the new runner logic — scanTargets file enumeration, absent: true inversion, the isAbsent branch at line 3765–3774 — has zero coverage. A bug in the condition !arrayLen(filesToScan) || arrayLen(matches) (line 3767) would not be caught by the current suite. Worth filing a follow-up to add a UpgradeCheckRunnerSpec with a small fixture project tree if the runner is extended further.
Commits
Single commit fix(cli): scan seven more 3.x → 4.x breakers in wheels upgrade check — valid fix type, cli scope, subject ≤ 100 chars, not ALL-CAPS. Conforms to commitlint.config.js.
Docs
CHANGELOG [Unreleased] entry is present and comprehensive. Framework docs and .ai/ reference updates are deferred to bot-update-docs.yml — acceptable given the PR template marks those checkboxes as deferred.
Wheels Bot — Reviewer B (round 1)A's review is substantively accurate and well-targeted. The three findings (RateLimiter advisory wording, CORS multi-line edge case, Adobe CF Elvis operator hazard) are all genuine and correctly characterised. One real gap stands out: A's test-coverage note under-states the problem by treating it as a follow-up concern rather than calling out the specific broken branch. No sycophancy and no false positives. SycophancyNone detected. A correctly gave COMMENTED rather than APPROVE on a draft bot PR with cross-engine and runner-logic concerns outstanding. False positivesNone detected. Each cited line was verified against the diff:
Missed issues
Verdict alignmentAligned. A's COMMENTED verdict without change requests is appropriate — the Adobe CF Elvis crash is the only issue that crosses into blocking territory for cross-engine CI, and A did identify it; the connection to the live crash path is the gap, not the verdict itself. ConvergenceA and I agree there are no architectural or correctness blockers outside the Adobe CF Elvis issue, which A raised. The one substantive gap is that A framed the Elvis crash as a latent maintenance trap rather than a live failure on every Adobe CF run of |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
This round adds a single docs-only commit (docs(web/guides): list all ten 3.x→4.x checks in upgrade command reference) on top of the previously reviewed implementation. The new commit correctly updates both v4-0-0 and v4-0-1-snapshot upgrade reference pages with all ten 3.x → 4.x breakers; the two files are byte-identical; and the commit conforms to commitlint. The implementation findings from the prior round remain unchanged — none rise to request-changes — so the verdict stays at comment.
Docs
Minor inaccuracy in the pre-existing "Each grep scans…" sentence. web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/upgrade.mdx line 113 (identical in the snapshot):
Each grep scans `.cfc` and `.cfm` files (as appropriate for the check) recursively
This sentence predates the PR, but the new wheels snippets check scans Makefile, package.json, .github/workflows/*.yml/.yaml, and top-level *.sh — none of which are .cfc or .cfm. A reader of the newly added bullet will correctly identify what files are scanned from the bullet itself, but the trailing prose now contradicts it. Worth a follow-up fix:
Each grep scan covers the file types relevant to that check (`.cfc`/`.cfm` for config and view checks; `Makefile`, `package.json`, `.yml`, `.yaml`, `.sh` for the build-script check) and reports `path:lineNumber` for every hit. Directory checks report only the top-level path when the directory exists and is non-empty.
Correctness (carried forward)
absent: true logic is correct. Re-verified: line 3767 !arrayLen(filesToScan) || arrayLen(matches) correctly resolves to PASS when (a) no scannable files exist (config/ missing → avoid noisy false positive) or (b) the pattern IS found (key is set). The else branch fires only when files were scanned and the pattern was never matched — which is the intended warning condition. No bug here.
RateLimiter check fires on all instantiations. cli/lucli/Module.cfc line 3631:
pattern: "new\s+wheels\.middleware\.RateLimiter",Any user who already set trustProxy and proxyStrategy explicitly will still receive the advisory. The fix description says "verify both flags are set" which is honest about the advisory nature, so this is intentional but worth documenting explicitly in CHANGELOG or the upgrade guide bullet as "advisory — fires regardless of current config."
Cross-engine (carried forward)
Latent Elvis ?: hazard on Adobe CF. cli/lucli/Module.cfc line 3739:
var exts = structKeyExists(target, "extensions") ? target.extensions : (check.extensions ?: "");check.extensions ?: "" crashes Adobe CF if the check struct lacks an extensions key at all. Today every scanTargets entry provides its own extensions (lines 3669–3671), making the fallback dead code. It becomes a maintenance trap when someone adds a new scanTargets check without extensions. Safer form consistent with .ai/wheels/cross-engine-compatibility.md:
var exts = structKeyExists(target, "extensions") ? target.extensions
: (structKeyExists(check, "extensions") ? check.extensions : "");Tests (carried forward)
The UpgradeCheckCoverageSpec.cfc uses static source inspection and provides coverage of all seven new patterns. The absent: true inversion logic, the scanTargets file enumeration, and the isAbsent branch remain without runtime test coverage. No new gaps introduced by the docs commit.
Commits
Both commits are well-formed:
fix(cli): scan seven more 3.x → 4.x breakers in wheels upgrade check— validfixtype,cliscope, ≤ 100 chars, DCO sign-off present.docs(web/guides): list all ten 3.x→4.x checks in upgrade command reference— validdocstype,web/guidesscope, ≤ 100 chars, DCO sign-off present.
Security
No concerns. The scanner is read-only; no user-controlled data flows into regex patterns or file paths.
- Replace Elvis `?:` on `check.extensions` in upgrade-check runner with `structKeyExists` ternary (cli/lucli/Module.cfc:3739). The previous form crashed Adobe CF whenever a `scanTargets` directory target lacked an `extensions` key and the parent check had no top-level `extensions` either — a live crash path on every Adobe CF run of `wheels upgrade check` reaching the `wheels snippets` check. - Strengthen RateLimiter check description and fix text so users recognise the scan is advisory and fires on every instantiation regardless of current config (cli/lucli/Module.cfc:3629). - Correct the "Each grep scans .cfc and .cfm files" prose in both upgrade.mdx pages — the new `wheels snippets` check also scans Makefile, package.json, .yml, .yaml, and .sh files. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Address Review (round 1)Applied consensus findings from Reviewer A and Reviewer B's convergence (round 1):
Skipped (not in the consensus, both reviewers explicitly characterised as follow-up rather than blocking):
Test note: The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on `approve` or the outer-loop cap (5 rounds) is reached. |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
This round adds a single follow-up commit (fix(cli): address Reviewer A/B consensus findings (round 1)) that resolves the two primary findings from the previous Reviewer A/B exchange. The implementation is now clean on all three points raised: the Adobe CF Elvis crash has been patched, the RateLimiter advisory language is explicit, and the upgrade guide prose accurately reflects the multi-format scan. No new issues are introduced; only minor pre-existing nits carry forward.
Correctness
Round-1 fix is correct. The exts fallback at cli/lucli/Module.cfc:3746 is now:
var exts = structKeyExists(target, "extensions") ? target.extensions
: (structKeyExists(check, "extensions") ? check.extensions : "");This is exactly the safe form recommended by .ai/wheels/cross-engine-compatibility.md struct-access guidance. Dead code today (every scanTargets directory entry provides its own extensions), but it no longer traps the next person who adds a bare directory target.
absent: true logic re-verified on current HEAD. cli/lucli/Module.cfc:3775:
if (!arrayLen(filesToScan) || arrayLen(matches)) {
arrayAppend(passed, check.description);
} else { ... issue ... }PASS when no files scanned (config/ absent — avoid false positive). PASS when key IS found. ISSUE only when files were scanned and the key was never seen. Semantics are correct.
Cross-engine
Adobe CF Elvis hazard is resolved. The prior marker noted:
check.extensions ?: ""crashes Adobe CF if the check struct lacks anextensionskey
This commit replaces it with structKeyExists. The comment on lines 3741-3745 explains the exact scenario (the wheels snippets check has no top-level extensions) so future maintainers understand the invariant. Well-handled.
Tests
No change to test coverage in this commit. The UpgradeCheckCoverageSpec.cfc static-inspection approach and the runtime gap for absent: true / scanTargets enumeration carry forward from prior rounds. Worth a follow-up UpgradeCheckRunnerSpec with a fixture project tree, but not blocking.
Docs
Both v4-0-0 and v4-0-1-snapshot upgrade.mdx files have the corrected sentence:
Each grep scan covers the file types relevant to that check (`.cfc` and `.cfm`
for config and view checks; `Makefile`, `package.json`, `.yml`, `.yaml`, and
`.sh` for the `wheels snippets` check) ...
This is accurate - the implementation at lines 3669-3672 confirms .yml, .yaml, and .sh are in scope. No remaining inaccuracy.
Commits
All three commits on this branch are well-formed:
fix(cli): scan seven more 3.x -> 4.x breakers in wheels upgrade check-fix,cli, <= 100 chars, DCO present.docs(web/guides): list all ten 3.x->4.x checks in upgrade command reference-docs,web/guides, <= 100 chars, DCO present.fix(cli): address Reviewer A/B consensus findings (round 1)-fix,cli, <= 100 chars, DCO present.
All conform to commitlint.config.js.
Wheels Bot — Propose FixNo triage comment found on #2645.
If the intent was to re-run If the intent was to keep iterating on this PR's review feedback, that lives Exiting without writing any code, opening any PR, or modifying the existing |
Wheels Bot — Reviewer B (round 2)A's round-2 review is accurate and well-structured. All three consensus findings from round 1 were applied correctly: the Adobe CF Elvis crash is patched, the RateLimiter advisory language is explicit, and the docs sentence now enumerates the actual file types per check. No sycophancy and no false positives. Two minor gaps worth noting. SycophancyNone detected. A correctly gave COMMENTED on a draft PR where the runtime test gap remains open. The round-1 findings were all addressed; COMMENTED is the right call.False positivesNone detected. All of A's cited lines are accurate. A writes the first commit subject using an ASCII arrow when the actual diff uses a Unicode arrow — immaterial to commitlint compliance and not a substantive false positive.Missed issues
Verdict alignmentAligned. Both A and I see no blocking issues on the current HEAD. The runtime test gap and the empty-pattern nit are follow-up concerns, not merge blockers.ConvergenceAll round-1 findings were applied. The remaining open items (runtime test coverage for `absent`/`scanTargets`/`isAbsent`, the empty-pattern nit) are non-blocking quality improvements. A and I agree on COMMENTED with no change requests. Converging on approve pending human promotion from draft. |
There was a problem hiding this comment.
Wheels Bot — Reviewer A response (round 2)
Both of B's round-2 missed-issue observations are valid and I concede both. No false positives were raised, so there is nothing to defend. My updated position matches B's: COMMENTED with no change requests, converging on approve pending human promotion from draft.
Conceded points
-
Empty
pattern: ""in thetests/specs/functions/directory check. B is correct. ThecheckType: "directory"branch at the runner level short-circuits before any grep loop, so the empty pattern is never evaluated today (cli/lucli/Module.cfclines 85–89 in the diff). However,reFindNoCase("", str)matches every string, meaning a future maintainer who routes this check through the grep path — or refactors the runner to unify branches — would get a silent false positive on every scanned line. A brief inline comment (// pattern unused — directory check; presence of the path is the signal) would make the intent unambiguous and prevent the trap. I missed this; B is right to flag it. -
"Dead code today" was imprecise language. B is correct that the outer
structKeyExists(target, "extensions")guard in thescanTargetsloop (cli/lucli/Module.cfclines 153–154 in the diff) executes on every target — it is live code. What is unreachable under current check definitions is the else-branch fallback tocheck.extensions. Calling the whole construct "dead code" was technically inaccurate and, as B notes, could encourage a future reviewer to prune it as cleanup. I should have written "thecheck.extensionsfallback is unreachable today" instead of "dead code today." I note that the inline comment in the code already explains the invariant explicitly (lines 148–152: "thewheels snippetscheck has no top-levelextensions"), which partially mitigates the framing risk; but B's precision point stands.
Defended points
None. B raised no false-positive claims against my prior findings.
Updated verdict
Still COMMENT with no change requests — the two conceded observations are quality follow-ups (a one-line comment and a terminology correction), not correctness or cross-engine blockers; the PR is ready for human promotion from draft.
Round-2 polish from Reviewer A/B exchange: B flagged that
`pattern: ""` in the tests/specs/functions/ directory check is a
silent trap if anyone unifies the directory and grep branches —
`reFindNoCase("", anyString)` matches every line. A conceded the
point and proposed a brief inline comment. Comment-only change;
no behavior diff.
Signed-off-by: Peter Amiri <peter@alurium.com>
Manual Address-Review (round 2) — PeterPicking this up by hand since the bot's convergence loop already landed on Changes pushed (
|
…s-upgrade-check-scans-only-3-of-11-documented Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # CHANGELOG.md
Merge from develop (
|
There was a problem hiding this comment.
Wheels Bot -- Reviewer A
Two new commits have landed since the round-2 A/B exchange closed: 46ee67c27 (comment-only polish on the empty-pattern trap) and 6df49c3ef (merge of origin/develop to incorporate CHANGELOG and other already-reviewed changes). Both are clean; the last open finding from prior rounds is now fully addressed. No new issues are introduced. The PR is ready for human promotion from draft.
Correctness
All previously noted correctness observations carry forward as resolved:
absent: trueinversion logic atcli/lucli/Module.cfc:3775-- re-verified correct on this head. PASS when no files scanned or when the key IS found; ISSUE only when files were scanned and the key is missing. Semantics unchanged.- Adobe CF Elvis hazard (
check.extensions ?: "") -- fixed inaefce0863(round-1 consensus).structKeyExistsguard is in place at lines 3746-3748 on the current head. - RateLimiter fires on all instantiations -- intentional and documented in the check description and
fix:text. Advisory behaviour.
Docs
The develop merge incorporated 02f891177 (docs(web/guides): clarify legacy adapter does not shim application.wirebox rename, PR #2636), which adds 23 lines to both v4-0-0 and v4-0-1-snapshot upgrade guides. Those additions are orthogonal to the upgrade-checker feature; the merge resolution is clean.
Commits
Five commits now on this branch, all well-formed:
fix(cli): scan seven more 3.x to 4.x breakers in wheels upgrade check--fix,cli, <=100 chars, DCO present.docs(web/guides): list all ten 3.x->4.x checks in upgrade command reference--docs,web/guides, <=100 chars, DCO present.fix(cli): address Reviewer A/B consensus findings (round 1)--fix,cli, <=100 chars, DCO present.fix(cli): document intentional empty pattern in directory check--fix,cli, <=100 chars, DCO present (Peter Amiri, human contributor sign-off).6df49c3efmerge commit -- DCO sign-off present.
All conform to commitlint.config.js.
Round-2 finding: resolved
The last conceded point from the round-2 A/B exchange was that pattern: "" in the tests/specs/functions/ directory check entry was a silent trap -- reFindNoCase("", anyString) matches every line, so a future refactor unifying the directory and grep branches would silently false-positive everywhere.
cli/lucli/Module.cfc:3677 on the current head:
// `pattern` is intentionally empty -- `checkType: "directory"` signals on
// path existence and never reaches the grep loop. Do NOT replace this
// with a benign regex: `reFindNoCase("", anyString)` matches every line,
// so a future refactor that unifies the directory and grep branches
// would silently false-positive on every scanned file otherwise.This is exactly the comment proposed in the round-2 concession. The intent is unambiguous and the trap is clearly flagged for future maintainers.
Tests (carried forward)
The UpgradeCheckCoverageSpec.cfc static-inspection approach covers all seven new patterns. The runtime gap for absent: true inversion and scanTargets file enumeration remains; a follow-up UpgradeCheckRunnerSpec with a fixture project tree is the right path, but is not blocking.
Wheels Bot — Reviewer B (round 3)A's round-3 review is accurate, concise, and well-scoped. It correctly confirms that all round-1 and round-2 findings have been addressed, verifies the five commits individually for commitlint compliance and DCO presence, and re-confirms the correctness of the SycophancyNone detected. A gave COMMENTED rather than APPROVE, which is the correct bot posture — the human collaborator (bpamiri) had already promoted the PR from draft before this review landed, but that does not change the bot's appropriate deference to human approval authority. COMMENTED is the right call. False positivesNone detected. Every observation in A's round-3 review maps to verifiable state in the diff:
Missed issues
Verdict alignmentAligned. A's COMMENTED with no change requests matches round 2's converged-approve conclusion. All substantive findings from prior rounds are closed. The runtime test gap ( ConvergenceRound 2 already landed a converged-approve marker. Round 3 confirms no regressions were introduced by the polish commit ( |
…es-to-and-dry-run-flag Pull in #2645 (wheels upgrade check: seven more 3.x → 4.x breakers) and #2637 (reloadPassword docs) which landed on develop after the previous merge (85549c3). CHANGELOG.md conflict resolved by keeping both Fixed entries adjacent — develop's #2628 entry alongside this PR's #2629 entry. cli/lucli/Module.cfc auto-merged cleanly: #2645 adds new check patterns around line 3679 (allowEnvironmentSwitchViaUrl, csrfEncryptionKey, viteStrictManifest, etc.); #2629's help-text changes sit at lines 224 and 2383-2399. Different regions, no overlap. Signed-off-by: Peter Amiri <peter@alurium.com>
Second develop catch-up — five more PRs landed on develop after the first merge (40d70f3): #2637, #2645, #2639, #2638, #2642. Re-resolves the same three documentation conflicts: - CHANGELOG.md: keep develop's #2628 and #2629 Fixed entries; move this PR's #2626 entry to the end of the Fixed list. - v4-0-0/upgrading/3x-to-4x.mdx: keep develop's new #2631 reloadPassword Common-issues bullet; this PR's #2626 static-asset bullet stays at the end. - v4-0-1-snapshot/upgrading/3x-to-4x.mdx: same resolution. cli/lucli/Module.cfc auto-merged again — no manual fixes needed. Signed-off-by: Peter Amiri <peter@alurium.com>
Summary
wheels upgrade check --to=4.0.0forcurrentMajor <= 3 && targetMajor >= 4previously surfaced only the legacyplugins/directory, thewheels.Testbase class, andapplication.wireboxreferences — 3 of the 11 documented breakers in the canonical 3.x → 4.0 upgrade guide. The tool's silence read as a green light, and users tripped over the unscanned breakers in production.This PR extends the 3.x → 4.x branch of
runUpgradeCheck()incli/lucli/Module.cfcwith seven additional scans:allowOrigins(deny-all default in 4.0, fix(security): change CORS default from wildcard to deny-all #2039)RateLimitermiddleware without explicittrustProxy/proxyStrategy(fix: change RateLimiter trustProxy default to false #2024, fix(middleware): default rate limiter proxy strategy to last for security #2088)allowEnvironmentSwitchViaUrl=true(default flipped in production, fix(config): default allowEnvironmentSwitchViaUrl to false in production #2076)csrfEncryptionKeyinconfig/— cookies rotate every deploy when empty (fix(security): auto-generate CSRF cookie encryption key when empty #2054)wheels snippetsinvocations inMakefile,package.json,.github/workflows/*.yml, and top-level*.sh(Rename 'snippets' command to 'code' across documentation and implemen… #1852)tests/specs/functions/directory rename (Rename tests/specs/functions/ to tests/specs/functional/ #1872)viteStrictManifestdefault flip throws on missing manifest entries in production (feat(view): transitive modulepreload and css resolution for vite pipeline #2133)The grep runner gains two backward-compatible options to support these checks:
scanTargets(mixed list of file paths + directory roots, needed by the multi-rootwheels snippetsscan) andabsent(warn when the pattern is NOT found anywhere, used by thecsrfEncryptionKeycheck). HSTS (#2081) and CSRFSameSite(#2035) remain advisory — there's no useful static signal in app config and a noisy "review HSTS" warning wouldn't help.Related Issue
Fixes #2628
Type of Change
Feature Completeness Checklist
Signed-off-by: claude[bot]vendor/wheels/tests/specs/cli/UpgradeCheckCoverageSpec.cfc(failing → passing). The spec statically inspects the 3.x → 4.x checks block incli/lucli/Module.cfcand asserts coverage of each breaker pattern.bot-update-docs.ymlbot-update-docs.ymlbot-update-docs.yml[Unreleased]wheels.tests.specs.cli:8 pass, 0 failonUpgradeCheckCoverageSpec(was1 pass, 7 failbefore the implementation); whole slice13 pass, 0 fail. The 5 pre-existing CLI-runner failures (AdminSpec,DoctorSpec,ScaffoldSpec) are unrelated to this change.Test Plan