Uh oh!
There was an error while loading. Please reload this page.
fix(cli-gate): name it.skip / it.only / it.each blocks instead of collapsing them onto (top-level) - #12577
Open
os-litant wants to merge 1 commit into
Open
fix(cli-gate): name it.skip / it.only / it.each blocks instead of collapsing them onto (top-level)#12577os-litant wants to merge 1 commit into
os-litant wants to merge 1 commit into
Conversation
…lapsing them onto (top-level)
callbackSiteName() required a bare-identifier callee, so `it('x', fn)` was named
but `it.skip('x', fn)` (a property access) and `it.each(table)('x', fn)` (a call)
returned null. enclosingFunctionName() then fell out of its loop to
'(top-level)', collapsing every skipped/only/table-driven block in one file onto
a single site key -- the same collision #12531 closed for the identifier branch,
one modifier over.
The unwrap is TARGETED, gated on two rosters rather than on node kind: the roots
it/test/describe, and the vitest modifiers that keep a chain a test block. A
blanket "property access whose base is an identifier" rule matches 209 call sites
across 87 distinct callees under packages/cli/test (literals.filter, child.on,
run.then, rows.map, vi.fn, ...) against this roster's 22, and naming those would
MOVE the key of every site nested inside one. The modifier roster is also what
refuses it.name / it.type / it.next -- ordinary loop variables in this directory
that happen to be spelled `it`.
The identifier branch is byte-identical, which DELIBERATE_REROUTE's two it("...")
keys pin in both directions: a green live-tree run is the proof that no existing
key moved.
Self-test 132 -> 152 cases. One pre-existing case changed: the one that pinned
it.skip AS (top-level), retargeted to the name it now produces; what its comment
was protecting (the refusal of promise.then / rows.map) is now pinned outright.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCdos-litant
marked this pull request as ready for review
August 26, 2026 11:50
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 freeto 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.
Fixes#12545
callbackSiteName()required a bare-identifier callee, soit('x', fn)was named butit.skip('x', fn)(aPropertyAccessExpression) andit.each(table)('x', fn)(aCallExpression) returnednull.enclosingFunctionName()then fell out of its loop to'(top-level)', collapsing every skipped / only / table-driven block in one file onto a single site key — the same collision #12531 closed for the identifier branch, one modifier over.Measured on
origin/main@aa4591971before touching anything:it('x', fn)Identifierit("x")it("x")(unmoved)it.skip('x', fn)PropertyAccess(top-level)it.skip("x")it.only('x', fn)PropertyAccess(top-level)it.only("x")describe.skip('x', fn)PropertyAccess(top-level)describe.skip("x")it.each([…])('x', fn)CallExpression(top-level)it.each("x")describe.each([…])('x', fn)CallExpression(top-level)describe.each("x")it.skip.each([…])('x', fn)CallExpression(top-level)it.skip.each("x")promise.then(cb)PropertyAccess(top-level)(top-level)(refused)rows.map(cb)PropertyAccess(top-level)(top-level)(refused)The unwrap is targeted, and it is gated on ROSTERS rather than on node kind
⛔ A blanket "property access whose base is an identifier" rule was measured, not assumed, and it is much worse than the defect. Against the gate's own live population (
packages/cli/test/**, 98 files), that rule matches:literals.filter,child.on,entries.map,warnings.filter,server.close,Array.from,vi.fn,run.then,rows.map…it.each(…)×20 anddescribe.each(…)×2, which is the entire modifier population of that directory.Those 87 are not test blocks, their callee is a worse site name than the enclosing block the walk already reaches, and naming them would move the key of every site nested inside one. Re-keying is a data migration wearing a bug fix's clothes.
So the gate is two rosters,
TEST_BLOCK_ROOTS×TEST_BLOCK_MODIFIERS:it,test,describe. ⭐ A roster on this side is load-bearing, not tidiness: this directory callsit.name,it.type,it.nextandit.prevVersionon ordinary loop variables that happen to be spelledit. Gating on the root alone would name a callback site after one of those.skip/only/todo/fails,concurrent/sequential/shuffle,each/for,skipIf/runIf. Every segment after the root must be on the roster, soit.skip.each(…)is named through the whole chain anda.b.c(cb)never is.each/forandskipIf/runIfcurry, and are unwrapped exactly one call deeper.extendis deliberately absent:test.extend(fixtures)builds a new test function, not a block, and its product is bound to a name the identifier branch already reads.The title is read from the outer call's arguments, so
it.each(['from the table'])('from the block', fn)is named for the block, never for a string sitting in the table. That is pinned.⭐ Key stability — the proof, stated rather than left to be noticed
The identifier branch is reached first and unchanged, so plain
it(…)naming is byte-identical. That matters becauseDELIBERATE_REROUTEholds two keys this function produced, carryingit("…")titles verbatim, andDELIBERATEholds two more from the variable-declaration branch — all four pinned in both directions, so a moved key exits 1.⇒ The green live-tree run below IS the proof that no existing key moved, and it says so in its own words:
And the pin was proven LOUD rather than assumed to be, because a green that cannot fail proves nothing. Reverse verification on the committed tree — predicted direction RED:
DELIBERATE_REROUTEkey (NODE_ENV=development (explicit)→NODE_ENV=deliberately-moved-key (explicit));1 → 0, mutant0 → 1, blob2210188b…→ec97c08b…;[it("NODE_ENV=development (explicit): the gate stays OPEN — unaffected by the production default")];git checkout HEAD --naming an ABSOLUTE path, and the restore was proven by state: blob back to2210188b…,git diff HEADempty.⛔ No registry entry was edited. Had one gone stale, that would have been the finding.
Self-test: 132 → 152 cases, extend-never-weaken
Exactly one pre-existing case changed, and it is the one that pinned the defect being fixed:
That is a strengthening — a specific name replaces a fallback — and what the old case's comment was really protecting (the refusal of
promise.then/rows.map) is now pinned outright, as six explicit refusal cases rather than as a side effect. Every other change is additive; the full deletion list in the diff is that one case plus the two implementation lines and the docblock limit it described.Added: each modifier form; both curried forms; the chained form; the table-string trap; two sibling
it.eachblocks getting distinct names; an end-to-endaudit()case where one registry entry silences oneit.eachblock and leaves its sibling a finding; the six refusals; and —⭐ the negative pin, which is the one that matters:
'a plain it() block STILL yields exactly it("x") — the identifier branch did not move'. Without it nothing in the suite distinguishes "the modifiers now work" from "everything got renamed".Rule 5 — the second implementation, MEASURED, not swept
scripts/check-durability-degradation-log-level.mjs:1695enclosingFunctionName(node), used at:2451and:2493. ⛔ Not touched here. What I read:It does NOT have this card's defect — and not because it handles modifiers, but because it has no callback-naming branch at all. Its four shapes are function declaration, method declaration, variable-bound arrow/function-expression, and named function expression. It never names
it('x', fn)either, so there is no identifier branch for a modifier unwrap to extend. The claim comment's guess that "it takes nosourceFileargument, so it may not have the same branches" is correct.Its fallback is
undefined, not'(top-level)', and the two call sites diverge::2493declaredPropagationFor— no defect, structurally.if (!fnName) return globalPropagation;short-circuits, so no key is ever built. That is a closed door, not a shared key.:2451→readInventionKey(:2827) — the same collision class, latent. It keys a shrink-only baseline onf.fileandf.fnjoined by::, substituting a fixed placeholder — the wordanonymouswrapped in angle brackets — wheneverf.fnis undefined. So two anonymous-callback read-seams in one file share one key, and a single baseline entry would classify both.Measured live:
scripts/durability-read-invention.baseline.jsonholds 1 entry, with 0 anonymousfnand 0 files carrying more than one entry. ⇒ latent, exactly as this card was. Filed unlabelled as its own card: #12576.Falsifications of the dispatch order (reported, not reconciled)
mainhad moved. The order pins the merged base at1b7e3d2ce;origin/mainwasaa4591971when I cut the branch (and9bed0b0feby the time gates ran). Re-verified the defect onaa4591971rather than trusting the description — it reproduced exactly.grepfinds 24 underpackages/cli/test/**, but two are inside docblocks (it.each` below against silently iterating nothing,it.each` over an empty). The AST count is 22. Fittingly, the suite's own section (6) exists to make exactly this point — "prose is not code, which is what an AST buys over a text scan".promise.then(cb)androws.map(cb)—promiseandrowsare identifiers — which is precisely the blanket widening the same ruling forbids two sentences later, and which the issue body names as the reason a blanket fix is worse than the defect. The issue body's own precise spelling ("it/test/describefollowed by a modifier") is the one that is self-consistent, so the implementation is roster-gated. The 209-vs-22 measurement above is what decided it. Flagging rather than silently choosing a side.:521,:1695,:2451,:2493, and the 132-case count) were correct. Surface measured as claimed:git grep -ln "callbackSiteName"→ exactly one file.packages/specfamilies printed bydispatch-gatesare the known [finding] 9packages/specgate families declare path populations the tree no longer has — a quiet green on every card, printed but never recorded #12514; not re-filed.anonymouskey literal) — inside backticks, and the eaten'anonymous'left the text asserting the fallback was an empty string, which is false. The first repair moved the literal into a fenced code block and that was eaten too, so a fence is no protection either. Both are now spelled in prose. ⛔ Worth knowing before quoting any angle-bracketed literal in a GitHub body.Changeset — rule applied, and which one
No changeset. AGENTS.md line 1108: "Pure bug fixes do not require a changeset." This is a repo-internal CI gate script under
scripts/, published by nothing and user-visible in no way; the change alters how a gate names sites in its own registry keys, not what it refuses.skip-changesetapplied — verified it is a real mechanism in this repo (.github/workflows/pr-automation.yml:294reads it live, not from the event payload) rather than assumed.Verification
typecheckis NOT APPLICABLE to a.mjsgate script — the roottsconfig.jsonsets noallowJs, sotsc --listFilesreturns 0 hits for this file. ⛔ Recorded as not-applicable, never as a green.All gates below run on the final commit
605beb0b3, exit codes captured before any pipe (cmd > log 2>&1; code=$?), underos-verify-lock(VERDICT command-exit 0 · held the lock 73s · waited 1s):check:cli-test-child-env(self-test 152 pass + live tree)check:agent-test-spellingcheck:bash32-floorcheck:cli-command-idscheck:cross-package-test-inputscheck:entry-guardcheck:parse-guardcheck:pnpm-filter-targetscheck:nul-bytescheck:pm-dispatch-gates(convention-triggered)scripts/check-ci-filter-parity.mjsscripts/check-cross-package-test-inputs.mjsscripts/pm/bare-root-worklist.mjs --self-test(convention-triggered)Families derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(10 path-derived + the 2 convention-triggered the order named). Control-character self-scan beyond the gate:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'→ no hits.Generated by Claude Code