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
Conversation
…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
ACCEPT — objectui#9013 / PR #9210Reviewed against the diff at 0. ⛔ The fence I wrote names a mechanism that does not existMy dispatch bound the round with this, and the card's own body carries it too:
There is no such floor. Measured on 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.
1. ⭐ The suggested direction, implemented literally, REDS the fixed treeBoth the card and my dispatch said: "require that it is a call to Neither half describes a single live call site. All four write: const env = childVitestEnv({ … }); // then passed as the shorthand property `env`verified on The round did not reason this out — it ablated it: disabling identifier resolution in the new 2. The hole is narrower than the card's prose impliesThe 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.
⇒ 3. What I verified in the diff
⭐ 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 — 4. One correction to me that improves the rule rather than just fixing itMy dispatch flagged the second
⇒ 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 inobjectui#9211, filed bare: the objectui#8598 gate's 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, Proceeding to the merge queue. Generated by Claude Code |
Armed — auto-merge
|
| anchor (code only) | <merge>^ |
<merge> |
|---|---|---|
envVerdict |
0 | 5 |
env.includes('childVitestEnv') |
1 | 0 |
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';
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
LANDED —
|
| 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
- ⛔ 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_FLOORfloors 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. - ⛔ 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 writeconst 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. - 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. - 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
Fixes #9013
The defect
scripts/__tests__/spawned-vitest-child-env-8616.test.tsderives 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 resolvedenv:text:resolved()hands that predicatenode.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:
spawned-build-vitest-env-8598.test.ts)VITESTmust be REMOVED from the child envchildVitestEnv()must be PRESENT as the child envenv: { ...process.env, VITEST: 'true' }— a tree that SETS the variable the gate exists to remove — passed, and the idiomatic rest-pattern scrub was REFUSED⇒ 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 tochildVitestEnv(), 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 anenv:and it is not that.absent— noenv: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:
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 diffcontains no+/-line matchingincludes('vitest').POPULATION_FLOOR = 1andNAMED_MEMBERare byte-identical.isAgentprobe and its control — byte-identical.⭐ The acceptance probe: reproduced, then inverted
The card's probe, verbatim. The two files differ by one comment line and nothing else (
diffreports5c5, one hunk). The probe was removed after every run and its absence verified withlseach time.1f4e02995a)4c4551610d)// A plain comment that names nothing in particular.1 failed | 4 passed, namingscripts/__tests__/probe-9013.test.ts:101 failed | 4 passed, same spawn, now with a reason// childVitestEnv() would be the right thing to use here.5 passed1 failed | 4 passed{ ...childVitestEnv(), CI: 'true' }5 passed, describe title reads5 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:
Two-sided ablation
Ran from the committed tree, under
trap … EXIT INT TERM.return 'helper'; // MUTANT-9013inserted as the first statement ofenvVerdict, making the new judgement incapable of refusing anything.MUTANT-9013grepped back at line 205 (matched line printed, not counted); blob moved72aac35ee17a576f0bf026a5bd8d53160793a771→11e33585c6385d9cd022f007c6c605969c7e156c. An empty hash was coded as FAILURE.5 passed, population5 vitest spawn(s). ⇒ the refusal in both probe rows comes fromenvVerdictand from nothing else.git checkout HEAD -- ABSOLUTE_PATH, then blob equality back to72aac35e…and an emptygit diff HEAD. Probe file absence re-verified.Gates run locally
Exit codes captured by redirect-then-capture, never through a pipe.
pnpm exec vitest run scripts/__tests__/spawned-vitest-child-env-8616.test.tsTest Files 1 passed (1)·Tests 5 passed (5)· exit 0scripts-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 0pnpm type-check:scriptstsc -p tsconfig.scripts.json --listFileslists the edited file at line 879, so this is a measurement and not an exclusionpnpm lint:root✖ 32 problems (0 errors, 32 warnings)· exit 0; 0 of them in the edited file.pnpm linthere isturbo run lint, ⛔ noteslint . --no-inline-configpnpm check:control-bytes✅ check-control-bytes: OK (scanned 7401 tracked text file(s); skipped 85 binary).pnpm check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0pnpm check:test-path-roots✅ check-test-path-roots: OKnode 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✅, exit 0The 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