Skip to content

test(scripts): judge the spawned-vitest child env by what it IS, not by whether the helper's name occurs in its text (objectui#9013) - #9210

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-9013-ast-child-vitest-env-membership
Sep 11, 2026
Merged

test(scripts): judge the spawned-vitest child env by what it IS, not by whether the helper's name occurs in its text (objectui#9013)#9210
baozhoutao merged 2 commits into
mainfrom
claude/issue-9013-ast-child-vitest-env-membership

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #9013

The defect

scripts/__tests__/spawned-vitest-child-env-8616.test.ts derives its population of vitest spawns by AST — that half is real, and its own header explains why a substring census of this class is not a census. It then judged each spawn's environment with a token-presence test over the resolved env: text:

const leaking = SPAWNS.filter((s) => s.env === null || !s.env.includes('childVitestEnv')).map(

resolved() hands that predicate node.initializer.getText(source). getText() spans from the initializer's start to its end, so a comment sitting inside the declaration — between the braces of { ...process.env, CI: 'true' } — is part of the text being compared. A spawn therefore passed by naming the helper. A child spawned that way inherits this container's agent markers, which is the entire defect objectui#8616 closed.

⛔ Not a duplicate of objectui#8712 — it is a NARROWER hole

Keeping the two apart is the point of the card, so it is stated here rather than collapsed:

objectui#8712, one gate over (spawned-build-vitest-env-8598.test.ts) objectui#9013, here
the goal VITEST must be REMOVED from the child env childVitestEnv() must be PRESENT as the child env
why presence was the wrong test wrong in both directions at once: env: { ...process.env, VITEST: 'true' } — a tree that SETS the variable the gate exists to remove — passed, and the idiomatic rest-pattern scrub was REFUSED presence of the helper genuinely is the property; the predicate was not wrong about what to look for
the reachable hole a call site that does the opposite of the fix only text that names the helper without calling it

⇒ there, the question itself was wrong. Here the question is right and the evidence was text, so the single reachable hole is a comment. Narrower — and, as the card measured, reachable.

The repair

The env: expression is now classified on nodes, the way objectui#8712's branch classifies its own env expression by what it does to the key:

  • helper — it is a call to childVitestEnv(), or an object literal that spreads one ({ ...childVitestEnv(), NO_COLOR: '1' }), reached directly or through the name the spawn hands the child.
  • foreign — there is an env: and it is not that.
  • absent — no env: property at all.

The resolved declaration's text is never consulted, so a comment about the helper resolves to nothing. Resolution stays deliberately narrow — the declaration of the name the spawn passes, and the names spread into it, never the whole file; anything wider answers "does this FILE mention childVitestEnv", which is the same question by a longer route.

The failure message now names which of the two ways a spawn leaks, and says that naming the helper is not using it.

⛔ What was NOT touched

The fences held, and each is checkable in the diff:

  • The AST population walk — untouched. In particular its cheap pre-filter if (!text.includes('vitest')) return []; is not the defect and was not "fixed for symmetry": it selects candidates for the AST pass, and every judgement is still made on the AST. git diff contains no +/- line matching includes('vitest').
  • The floor and the named memberPOPULATION_FLOOR = 1 and NAMED_MEMBER are byte-identical.
  • The live isAgent probe and its control — byte-identical.
  • The four call sites objectui#8616 protects — not edited at all. The population is still 4 spawns and all four still pass, so no count over them moved down.

⭐ The acceptance probe: reproduced, then inverted

The card's probe, verbatim. The two files differ by one comment line and nothing else (diff reports 5c5, one hunk). The probe was removed after every run and its absence verified with ls each time.

probe before (1f4e02995a) after (4c4551610d)
control// A plain comment that names nothing in particular. exit 1, 1 failed | 4 passed, naming scripts/__tests__/probe-9013.test.ts:10 exit 1, 1 failed | 4 passed, same spawn, now with a reason
hole// childVitestEnv() would be the right thing to use here. exit 0, 5 passed exit 1, 1 failed | 4 passed
accept (added here) — { ...childVitestEnv(), CI: 'true' } exit 0, 5 passed, describe title reads 5 vitest spawn(s)

⭐ The control is load-bearing: it proves the probe really is in this gate's population and really is refused when the helper is absent, so the green in row two was the predicate accepting it, not the walk failing to see it. After the fix the control is unchanged — the walk still sees the probe.

⭐ The accept row is the other half, and it is why the "after" column is not just a gate that reds at everything: with the helper actually spread, the same probe is admitted to the population (5 vitest spawn(s) in the describe title) and passes.

Verdict lines, quoted from the runs:

before/hole:    Test Files  1 passed (1)   /   Tests  5 passed (5)          exit 0
after/hole:     FAIL ... > every one of them builds the child environment with childVitestEnv()
                scripts/__tests__/probe-9013.test.ts:10 — this `env:` is not `childVitestEnv()` and does not spread one
                Test Files  1 failed (1)   /   Tests  1 failed | 4 passed (5)   exit 1

Two-sided ablation

Ran from the committed tree, under trap … EXIT INT TERM.

  • Mutation: return 'helper'; // MUTANT-9013 inserted as the first statement of envVerdict, making the new judgement incapable of refusing anything.
  • Landed on disk: anchor MUTANT-9013 grepped back at line 205 (matched line printed, not counted); blob moved 72aac35ee17a576f0bf026a5bd8d53160793a77111e33585c6385d9cd022f007c6c605969c7e156c. An empty hash was coded as FAILURE.
  • Direction: with the control probe present — the one the fixed gate must refuse — the mutant went green, 5 passed, population 5 vitest spawn(s). ⇒ the refusal in both probe rows comes from envVerdict and from nothing else.
  • Restored: git checkout HEAD -- ABSOLUTE_PATH, then blob equality back to 72aac35e… and an empty git diff HEAD. Probe file absence re-verified.

Gates run locally

Exit codes captured by redirect-then-capture, never through a pipe.

command verdict line
pnpm exec vitest run scripts/__tests__/spawned-vitest-child-env-8616.test.ts Test Files 1 passed (1) · Tests 5 passed (5) · exit 0
7 neighbouring gate test files (incl. the objectui#8598 sibling, scripts-type-check, tsconfig-test-parity-census, check-changeset-presence, check-control-bytes, zero-test-member-exclusion-9106) Test Files 7 passed (7) · Tests 138 passed (138) · exit 0
pnpm type-check:scripts exit 0; tsc -p tsconfig.scripts.json --listFiles lists the edited file at line 879, so this is a measurement and not an exclusion
pnpm lint:root ✖ 32 problems (0 errors, 32 warnings) · exit 0; 0 of them in the edited file. ⚠️ pnpm lint here is turbo run lint, ⛔ not eslint . --no-inline-config
pnpm check:control-bytes ✅ check-control-bytes: OK (scanned 7401 tracked text file(s); skipped 85 binary).
pnpm check:new-line-citations VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0
pnpm check:test-path-roots ✅ check-test-path-roots: OK
node scripts/check-changeset-presence.mjs ✅ No source or published contract of a released package changed in this range, so no changeset is owed.
check-changeset-fixed / -no-major / -overwrite / check:changeset-claims all , exit 0

The changeset carries empty frontmatter: test-only, no package is released.

CI's required contexts — Test (shard N/4), Type Check, Lint, Changeset Declaration, Control Byte Scan — are left to CI; their verdict lines are not quoted here because they had not reported when this was opened.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…by whether the helper's name occurs in its text (objectui#9013)

`scripts/__tests__/spawned-vitest-child-env-8616.test.ts` derived its
population of vitest spawns by AST — that half is real and its header
explains why a substring census of this class is not a census — and then
judged each spawn with a token-presence test over the RESOLVED `env:` text:

    SPAWNS.filter((s) => s.env === null || !s.env.includes('childVitestEnv'))

A comment sitting inside the declaration's span is part of that text, so a
spawn passed by NAMING the helper. Measured on this branch's parent, with two
probe files differing by one comment line and nothing else:

  control — `// A plain comment that names nothing in particular.`
    exit 1, `1 failed | 4 passed`, naming the probe's spawn
  hole    — `// childVitestEnv() would be the right thing to use here.`
    exit 0, `5 passed`

The control is the load-bearing half: it proves the probe really is in the
population and really is refused when the helper is absent, so the green in
the second row is the predicate accepting it, not the walk failing to see it.

Now the `env:` expression is classified on NODES — `helper` when it IS a call
to `childVitestEnv()` or an object literal that spreads one, reached directly
or through the name the spawn passes; `foreign` when there is an `env:` that
is not that; `absent` when there is none. The resolved declaration's text is
never consulted, so a comment about the helper resolves to nothing. Same shape
as objectui#8712's repair one gate over, and not the same hole: there presence
was the wrong test in both directions because the goal was REMOVAL, here
presence of the helper IS the goal, so the only reachable hole is text that
names it without calling it.

The population walk, its cheap `text.includes('vitest')` pre-filter, the
floor, the named member and the live `isAgent` probe are untouched. The
population is still 4 spawns and all four still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Empty frontmatter: no package is released by this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@github-actions github-actions Bot added the tests label Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — objectui#9013 / PR #9210

Reviewed against the diff at 4c4551610de73b14272d1fb40d5be86ec8acbb49; ls-remote, the report's sha and the branch head agree.

0. ⛔ The fence I wrote names a mechanism that does not exist

My dispatch bound the round with this, and the card's own body carries it too:

Must not reduce the assertion counts of the four pinned files objectui#8616 already protects — a floor exists over them.

There is no such floor. Measured on origin/main: POPULATION_FLOOR appears exactly three times under scripts/__tests__vite-dts-wiring-ratchet (15), spawned-build-vitest-env-8598 (1), and this gate (1) — and what this gate's floors is:

expect(SPAWNS.length).toBeGreaterThanOrEqual(POPULATION_FLOOR);
expect(SPAWNS.map((s) => s.file)).toContain(NAMED_MEMBER);

⇒ it floors the spawn population, ⛔ not anybody's assertion count. Nothing in the tree reads the four call-site files as a ratchet.

So the fence was unfalsifiable as written — a reviewer asked to check it would have had nothing to check. The intent was right and the round honoured it (population still 4, all four still pass, no pinned file edited), but a fence that cannot be checked is decoration.

⚠️ And it is the second time today I have done exactly this: on objectui#8694 I repeated the card's --resync sentence without opening the flag, and the file's own header said "The ledger REGISTERS a file; --resync REWRITES one. Those are different." Both times I inherited a sentence about a mechanism and passed it on as a constraint without reading the mechanism. ⇒ ⛔ a claim about what the code enforces is a claim, not a fence, until the enforcing line is quoted. That goes into the template with the enforcing line required inline.

1. ⭐ The suggested direction, implemented literally, REDS the fixed tree

Both the card and my dispatch said: "require that it is a call to childVitestEnv (or spreads one), by AST."

Neither half describes a single live call site. All four write:

const env = childVitestEnv({});   // then passed as the shorthand property `env`

verified on origin/main at all four — check-vi-mock-inherit.test.ts, dist-pins-guard-message-8274.test.ts, network-escape-worker-coverage-8537.test.ts, vitest-timezone-pin-8366.test.ts. A shorthand property whose name resolves to a declaration whose initializer is the call.

The round did not reason this out — it ablated it: disabling identifier resolution in the new envVerdict (blob 72aac35e76b0b63d, restored to blob equality and an empty git diff HEAD) reds all four. ⇒ name resolution is the load-bearing third clause of a sentence that had two.

⚠️ And the sharpest part: the sibling gate's own header records having hit this exact failure"Measured: it did, on both call sites, before this resolved." It was on the page my dispatch told the dev to read, and the suggested direction still dropped it. ⛔ A direction copied from a sibling gate is not the same as reading what the sibling gate learned.

2. The hole is narrower than the card's prose implies

The card says "a comment sitting inside the declaration's span is part of that text." True — and the probe puts the comment in the only place where it is true. ts.Node.getText() starts at getStart(), which skips leading trivia. Measured with the repo's own typescript on three placements of the identical comment:

placement initializer.getText() contains childVitestEnv
between the braces of the env object ✅ TRUE — the reachable spelling
on the line above const CHILD_ENV = … ❌ FALSE
between the = and the { ❌ FALSE

⚠️ someone re-deriving the defect from a comment placed above the declaration would have measured a red and concluded the card was wrong. The card's probe is correct; the generalisation a reader takes from its prose is not.

3. What I verified in the diff

claim how verdict
scope 22 0 changeset + 106 10 gate file ✅ two files, no third
⛔ the population walk is untouched POPULATION_FLOOR/NAMED_MEMBER still 2 declarations; the pre-filter still reads if (!text.includes('vitest')) return []; ✅ byte-identical (its line moved 144 → 233 — ⛔ movement, not change)
the judgement is now on nodes base: SPAWNS.filter((s) => s.env === null || !s.env.includes('childVitestEnv')) → head: SPAWNS.filter((s) => s.verdict !== 'helper'); envVerdict 0 → 5 occurrences ✅ two-sided
⛔ the text judgement is really gone ⚠️ grep -c "env.includes('childVitestEnv')" returns 1 on both sides — so I printed the line: on the head it is :56, header prose documenting what was replaced. Code-only count: 0. ✅ — and ⭐ a bare count would have read as "the text judgement survived". On this card that is the subject matter, not the method

The third probe is the round's own addition and it is the right one. Reproducing the card's two probes shows the hole closing; a third — { ...childVitestEnv(), CI: 'true' } — comes back exit 0, 5 passed, with the population reading 5 vitest spawn(s). ⇒ the probe was admitted to the population AND passed, which is what stops the after-column from reading as "a gate that now reds at everything". ⛔ Two probes would have proven the refusal without proving the acceptance.

4. One correction to me that improves the rule rather than just fixing it

My dispatch flagged the second includes as a trap: "a cheap pre-filter inside the population walk — do not touch it." Right conclusion, weaker reason than the real one:

That includes is over the WHOLE FILE and admits candidates to the AST pass, so comment-inclusiveness there can only ever ADD candidates. Making it precise would REMOVE members from the population — which is not "symmetry with the fix", it is the weakening of the AST half triage fenced, arriving through the door marked "consistency."

⇒ that sentence is now attached to the trap in the template. A "don't touch it" with a direction attached survives a reader who disagrees; one without it does not.

5. Routed, ⛔ not folded in

objectui#9211, filed bare: the objectui#8598 gate's SPAWNERS set omits fork while its objectui#8616 sibling includes it — a false-green direction, latent today (0 fork( call sites in the test tree against a same-run control of 35 spawnSync( hits). ⭐ An absence with a control that hits, which is the only form in which "latent" is a reading.

6. Not overlooked

⛔ CI's required contexts had not reported when the round finished, and it correctly quoted no verdict line for them rather than presenting local runs as CI's. The local evidence is substantial (7 neighbouring gate files 138/138, type-check:scripts with --listFiles showing the edited file in the program, lint:root 0 errors) and it is labelled as local.

Proceeding to the merge queue.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 11, 2026 19:20

Copy link
Copy Markdown
Contributor Author

Armed — auto-merge SQUASH, and the probe I will run afterwards, written first

Armed 2026-09-11T19:20:56Z on head 4c4551610de73b14272d1fb40d5be86ec8acbb49. ⓘ The arm call echoed method: MERGE; ⛔ not the authority — origin/main is linear, so the queue squashes.

The probe, posted BEFORE the merge

Every value extracted from the tree at 4c45516 and 1f4e0299 just now, ⛔ not retyped from the report. Legs scoped to <merge>^ <merge>, ⛔ never origin/main, and every read names an explicit ref — this seat's working checkout is ~580 commits stale, so a bare grep reads 2026-09-06.

L1 — the file set. Exactly 2 paths: scripts/__tests__/spawned-vitest-child-env-8616.test.ts and .changeset/9013-ast-child-vitest-env-membership.md. Counts 106 10 and 22 0.

L2 — the judgement swap, positive in both directions. ⭐ Counted over code lines only — comment lines excluded — because on this card that filter is the subject matter:

anchor (code only) <merge>^ <merge>
envVerdict 0 5
env.includes('childVitestEnv') 1 0

⚠️ The comment-only filter is load-bearing here and I nearly published without it. A bare grep -c "env.includes('childVitestEnv')" returns 1 on both sides — which reads as "the text judgement survived". Printing the line shows the head's hit is :56, header prose recording what was replaced, which is exactly what that header should say. ⛔ The count was the wrong instrument; the printed line was the reading.

L3 — ⛔ what must STAY: the AST population walk, which triage fenced. All three byte-identical across the merge:

if (!text.includes('vitest')) return [];
const POPULATION_FLOOR = 1;
const NAMED_MEMBER = 'scripts/__tests__/network-escape-worker-coverage-8537.test.ts';

⚠️ The pre-filter's line number moves (144 → 233) — the mechanical consequence of the new code above it. ⛔ Movement, not change, which is why this leg compares strings and never a path:line.

⭐ And the direction matters, in the round's own better words: that includes is over the whole file and admits candidates to the AST pass, so comment-inclusiveness there can only ever add members. Making it precise would remove them — the weakening triage fenced, arriving through the door marked "consistency".

L4 — the four call sites objectui#8616 protects were not edited. git diff --name-only <merge>^ <merge> must contain none of check-vi-mock-inherit.test.ts, dist-pins-guard-message-8274.test.ts, network-escape-worker-coverage-8537.test.ts, vitest-timezone-pin-8366.test.ts. ⭐ Its control is L1: the file set is 2, so this is an absence with a positive beside it rather than an unfalsifiable "nothing else changed".

L5 — nothing moved in transit, by blob.

path expected
scripts/__tests__/spawned-vitest-child-env-8616.test.ts 72aac35ee17a576f0bf026a5bd8d53160793a771
.changeset/9013-ast-child-vitest-env-membership.md da8e4db924f957b7ae65961293c1232a4a7b4178

⭐ Free cross-check: 72aac35e is the same blob prefix the report quotes as the pre-mutation baseline of both ablations. The round's ablations and my read of the tree describe one object.

L6 — the changeset is an explicit no-release declaration, not a missing one. Its frontmatter must be empty (--- / ---), which in this repo is the declaration rather than an omission. Read on the head; a test-only change owes no bump.

⛔ NOT MEASURED: the gate's own run, the probe reproduction, and both ablations. Bare stale checkout — and last round taught me that a runnable gate here answers fluently about the wrong tree. CI is the reading; ⛔ its required contexts had not reported when the round finished and the report correctly quoted no verdict line for them.

A leg that comes back amber will be published with its cause. ⛔ It will not be re-run into green.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit ad80b06 Sep 11, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-9013-ast-child-vitest-env-membership branch September 11, 2026 19:50

Copy link
Copy Markdown
Contributor Author

LANDED — ad80b06ff2

Probe run scoped to ad80b06ff2^ ad80b06ff2. All six legs green.

leg reading
L1 file set 2 — the gate file and its changeset ✅
L2 the judgement swap, code-only envVerdict 0 → 5; env.includes('childVitestEnv') 1 → 0
L3 ⛔ what must STAY the pre-filter if (!text.includes('vitest')) return [];, POPULATION_FLOOR = 1, NAMED_MEMBER — all 1 → 1, byte-identical ✅
L4 the protected call sites 0 of the four objectui#8616 files appear in the diff ✅, with L1's count of 2 as its control
L5 nothing moved in transit 72aac35ee17a576f0bf026a5bd8d53160793a771 + da8e4db924f957b7ae65961293c1232a4a7b4178
L6 the changeset declares frontmatter empty ✅ — the explicit no-release form, not an omission

⭐ L2 is why the comment filter is not a formality, and the probe shows it rather than says it

The raw grep, printed from both sides of the merge:

ad80b06ff2^   grep -c "env.includes('childVitestEnv')"  =  1
ad80b06ff2    grep -c "env.includes('childVitestEnv')"  =  1

⇒ a bare count reads unchanged"the defect survived". The matched line on the merge commit:

56: *     judgement was `env.includes('childVitestEnv')`: two probe files differing

Header prose, recording what was replaced — which is exactly what that header should say. Code-only count: 0. ⛔ The count was the wrong instrument; the printed line was the reading. On a card whose entire subject is a gate that judged text instead of nodes, that is not a coincidence.

The corrections this round made to me, kept on the record

  1. My fence named a mechanism that does not exist. I bound the round with "must not reduce the assertion counts of the four pinned files — a floor exists over them." There is no such floor: POPULATION_FLOOR floors the spawn population (expect(SPAWNS.length).toBeGreaterThanOrEqual(…)), and nothing reads those four files as a ratchet. ⇒ the fence was unfalsifiable. New rule: a claim about what the code enforces is a claim, ⛔ not a fence, until the enforcing line is quoted inline.
  2. The suggested direction, implemented literally, reds the fixed tree. "Require it IS a call to childVitestEnv (or spreads one)" describes none of the four live sites — all write const env = childVitestEnv(…) and pass shorthand { env }. Proved by ablation, not argument: disable identifier resolution, all four go red. ⚠️ And the sibling gate's own header records having hit this — it was on the page my dispatch sent the round to read.
  3. The hole is narrower than the card's prose. getText() skips leading trivia, so only a comment between the braces opens it; one placed above the declaration would have measured red and made the card look wrong.
  4. My trap warning needed a direction. The right reason to leave the pre-filter alone: it is over the whole file and admits candidates, so comment-inclusiveness there can only add members — making it precise would remove them, "the weakening triage fenced, arriving through the door marked consistency."

⭐ And the round's own addition: a third probe, { ...childVitestEnv(), CI: 'true' }, admitted to the population and passing. Two probes prove refusal; the third proves acceptance, and without it the after-column reads as "a gate that now reds at everything."

Closing objectui#9013. objectui#9211 filed bare: the sibling objectui#8598 gate's SPAWNERS omits fork where this one includes it — false-green direction, latent, with a control that hits (0 fork( against 35 spawnSync().


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(scripts): the objectui#8616 gate accepts a vitest spawn that only MENTIONS childVitestEnv in a comment

2 participants