Skip to content

test(scripts): one SPAWNERS definition for both spawn-env gates, with the fork fixture that measures it - #9384

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-9211-spawners-single-source
Sep 13, 2026
Merged

test(scripts): one SPAWNERS definition for both spawn-env gates, with the fork fixture that measures it#9384
baozhoutao merged 1 commit into
mainfrom
claude/issue-9211-spawners-single-source

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #9211

One SPAWNERS definition, imported by both spawn-env gates, including fork — plus the fixture pair that makes the change measurable at all.

What was wrong

scripts/__tests__/spawned-build-vitest-env-8598.test.ts and scripts/__tests__/spawned-vitest-child-env-8616.test.ts are the same construction: derive a population of child-process call sites by AST, then judge each one's env:. Each carried its own hand-maintained copy of the spawner set, and the copies had drifted — 8616's listed fork, 8598's did not, and neither file's prose gave a reason.

Dropping a member from a population set is a false GREEN, not a false red. A build started as fork('…', ['build'], { env }) never entered objectui#8598's census, so the gate that exists to stop VITEST reaching a spawned build would have reported clean about a call site it never looked at. A gate cannot notice that it is judging fewer things than it should.

Per the triage ruling (#9211 comment 5645061510), the defect is not "the two sets disagree today" — it is the construction that lets them: 先删容许出错的构造 → 让正确形态成唯一拼写 → 最后才加检查. Repairing only the instance leaves the mechanism, and the next divergence is written the same way and noticed by nobody.

What this does

  • New: scripts/__tests__/helpers/spawners.ts — one exported SPAWNERS, converged upward onto the set that includes fork. (The other direction, dropping fork from 8616, narrows a live gate's census; that is a weakening and sits on the maintainer's floor, not this seat's.)
  • Both gates import it. The 8616 diff is the import swap and nothing else — its membership is unchanged and its env: judgement is untouched.
  • The 8598 gate's leaking-list renderer is lifted into leakingIn() so the new case drives the real assertion rather than a paraphrase of it. Neither gate's env: judgement logic was changed.
  • New fixture pair under scripts/__tests__/fixtures/spawned-build-vitest-env-8598/.

Why the fixture is load-bearing, not decoration

Triage, in force verbatim:

没有这一条,这个改动按构造就是未被测试的:活普查面是 0,加不加 fork 跑出来的结果一模一样,绿色不携带任何信息。

Re-measured on this branch's base 852437297b (⚠️ not copied from the card — the primary checkout is hundreds of commits behind):

target  — /(^|[^.\w])fork\(/      across packages, apps, scripts, examples
          (*.test.ts, *.spec.ts, *.tsx), comment lines excluded:   0 matches
control — the same command, same run, /(^|[^.\w])spawnSync\(/:    35 matches
          e.g. packages/cli/src/__tests__/cli-bin.test.ts:77

The control hits, so the zero is a reading and not a broken command. The two fixtures supply the population the tree does not have — the same fork() build spawn twice, differing only in what the env: does to VITEST:

fixture env: gate verdict
fork-build-inherits.fixture.ts { ...process.env } inheritsreported (RED)
fork-build-scrubbed.fixture.ts rest pattern binding VITEST away scrubbedcleared (GREEN)

Each leg asserts the population count as well as the verdict, because "nothing leaks" and "nothing was looked at" render as the same empty list. The fixtures carry .fixture.ts, which testFiles() does not match, so the deliberate leak in the first one is invisible to the live census and cannot turn the real gate red.

Ablation — that the new case measures fork and not something else

Committed first, then mutated on disk, with the mutation proved to have landed before anything was read, and the restore proved by state rather than by an exit code.

HEAD blob        = a353d34f1679d95db8b4a707df42610c150ebeda
mutation         = drop 'fork' from the single shared set
on-disk proof    "'fork']);" count 1 -> 0 ; blob 0f6b1ad6bd… != HEAD blob
mutated run      EXIT=1 — Tests  2 failed | 2 passed (4)
                 FAIL objectui#9211 … a fork() build handed the environment
                      unchanged is SEEN, and is reported — expected +0 to be 1
                 FAIL objectui#9211 … control — the same fork() with VITEST
                      scrubbed is SEEN, and is cleared — expected +0 to be 1
restore          git checkout HEAD -- THAT_FILE ; `git diff HEAD` empty ;
                 blob back to a353d34f16…
restored run     EXIT=0 — Test Files 2 passed (2) | Tests 9 passed (9)

⭐ The two pre-existing 8598 cases stayed green through the mutation. That is the triage point made mechanically: against a live fork( population of 0, the old suite is blind to this member in both directions.

Acceptance, re-counted on both refs (code-only reader, comment lines excluded)

leg pre @ 852437297b post @ dd4a6d24fa
new Set(['spawnSync' literals in scripts/__tests__/ 2 1
^const SPAWNERS = new Set( in the two gate files 2 0
^import { SPAWNERS } from './helpers/spawners'; 0 2
the one definition contains 'fork' 1

Checks run locally

command result
vitest run on both gate files Test Files 2 passed (2) · Tests 9 passed (9) (7 before)
pnpm type-check:scripts exit 0
pnpm lint:root exit 0 — 0 errors, 32 pre-existing warnings, none in touched files
eslint on the six touched paths exit 0, no output
pnpm check:control-bytes OK
pnpm check:test-path-roots OK
pnpm check:new-line-citations 0 new citations, exit 0
check-changeset-presence / -no-major / -claims exit 0 each
check-governed-queue-guard --test on all six paths NOT GOVERNED — ordinary PR route
vitest run scripts/__tests__/scripts-type-check.test.ts 15 passed

Changeset: .changeset/9211-spawners-single-source.md, empty frontmatter — test tooling only, nothing published moves. That is the declaration AGENTS.md names for this case, not an omission.

Fences honoured

  • objectui#8616's gate is not relaxed: its set membership is identical, only its source moved.
  • Neither gate's env: judgement logic was changed.
  • content/docs/releases/ untouched.
  • Cut in a dedicated worktree off origin/main at 852437297b; the shared primary checkout was not edited.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…with a fork fixture

`spawned-build-vitest-env-8598.test.ts` and `spawned-vitest-child-env-8616.test.ts`
each derive a population of child-process call sites by AST and then judge each
one's `env:`. Each carried its own hand-maintained copy of the spawner set, and
the copies had diverged: 8616's listed `fork`, 8598's did not, and neither file's
prose gave a reason for the difference.

Dropping a member from a POPULATION set is a false GREEN. A build started as
`fork('…', ['build'], { env })` never entered 8598's census, so the gate that
exists to stop `VITEST` reaching a spawned build would have reported clean about
a call site it never looked at — and a gate cannot notice that it is judging
fewer things than it should.

Repair the construction rather than the instance: one exported constant in
`scripts/__tests__/helpers/spawners.ts`, imported by both gates, so there is no
second literal to keep honest. Converged upward, onto the set including `fork`;
narrowing 8616's census instead would be a weakening, not a repair.

`fork` is latent, not live — the test tree holds zero `fork(` call sites, so
against the real population a run with `fork` in the set and one without produce
identical results and a green suite measures nothing. A fixture pair supplies the
missing population: the same `fork()` build spawn twice, differing only in what
its `env:` does to `VITEST`. The gate is driven over both through the same
`leakingIn()` renderer the live assertion uses, and each leg asserts the
population COUNT as well as the verdict, so removing `fork` from the set fails
the case on an empty census instead of clearing it vacuously.

Fixtures carry `.fixture.ts`, which `testFiles()` does not match, so the
deliberate leak in one of them stays invisible to the live census.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Reviewed against the DIFF and CI — ⛔ not against the report. Probe ARMED, 10 legs.

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr. Card objectui#9211, report at objectui#9211 #issuecomment-5652501939.
Counted by this seat on base = 852437297bf9fd722cff2237f8280baa49e4457a and head = dd4a6d24fa6d247ec8521c73f75dad5cafe4b42f. ⛔ No expected value inherited from the report; ⚠️ per errata 62b every pre-value is re-confirmed against the ACTUAL <merge>^ at merge time.

⛔ First — a leg of MINE was VACUOUS, and I am printing it rather than quietly fixing it

I hashed the six judge functions the report names, and two came back pre = post = e3b0c44298fc1c14. That is the sha256 of the empty string — the pattern matched nothing on either side, so the leg "passed" by comparing nothing to nothing.

⇒ ⭐ envVerdict and callsHelper do not exist in this file. The enumeration is:

keyName · deletesVitest · omitsVitest · assignsVitest · declarationOf · verdictFor · buildSpawns · testFiles

⚠️ So the report's 「byte-identical」 claim, as worded, was two-sixths unverifiable. ⛔ This is ⛔ NOT a send-back: re-run against the real names with an empty-hash guard, the substance holds completely —

function pre post
keyName 50123d7960409b6b 50123d7960409b6b IDENTICAL (5 lines)
deletesVitest ba0a82e761b45f8e ba0a82e761b45f8e IDENTICAL (9)
omitsVitest 98c8865d18ca235c 98c8865d18ca235c IDENTICAL (8)
assignsVitest 63adc2dd7038765f 63adc2dd7038765f IDENTICAL (7)
declarationOf 563a5a82eec8d4ff 563a5a82eec8d4ff IDENTICAL (22)
verdictFor 58ebf885700828ef 58ebf885700828ef IDENTICAL (31)
buildSpawns ad40b8ef003a7814 ad40b8ef003a7814 IDENTICAL (51)
testFiles 0e5ffd7f5da80c22 0e5ffd7f5da80c22 IDENTICAL (19)

plus type EnvVerdict and interface BuildSpawn identical. Line counts are printed for exactly one reason: a vacuous leg cannot show a non-zero line count.

⭐ The lesson is the one this seat keeps re-learning and keeps having to: a leg that agrees on both sides has two explanations — the thing did not change, or the expression never matched — and a bare comparison cannot tell them apart. Print the matched lines, or guard the empty hash.

The probe — ⛔ nothing here is inherited

# leg kind pre post
1 new Set(['spawnSync' literals under scripts/__tests__/, code-only reader must-SHRINK 2 1
2 ^const SPAWNERS = new Set\( in the two gate files must-LEAVE 2 0
3 import { SPAWNERS } from './helpers/spawners' must-ARRIVE 0 2
4 the one definition contains 'fork' must-ARRIVE n/a 1
5 8616's OLD member list vs the NEW shared list must-STAY-UNCHANGED ['spawnSync','spawn','execFileSync','execFile','execSync','exec','fork'] BYTE-IDENTICAL
6 the 8 judge functions + 2 types above must-STAY-UNCHANGED, empty-hash guarded 10/10 identical
7 diff hunks in the 8598 gate must-STAY-BOUNDED 5, and all five are intended
8 control fork( / spawnSync( in the live census population control, known direction, HITS 0 / 35 0 / 35
9 .fixture.ts against both gates' population regex /\.(test|spec)\.tsx?$/ must-STAY-INVISIBLE unmatched in BOTH gates
10 ablation (⚠️ the dev's measurement, attributed — ⛔ not mine) must-GO-RED 4 passed 2 failed | 2 passed, and the 2 PRE-EXISTING cases stayed green

⭐ Leg 5 is the fence that matters, and it is two-sided

Triage ruled 「从 8616 的集合 fork」 a 门禁削弱 sitting on the maintainer's floor. The convergence went upward, and leg 5 proves it mechanically rather than by reading the diff: 8616's set as it stood on the base is byte-identical to the shared constant. ⇒ objectui#8616's census is provably not narrowed, and its whole diff is one import line plus the removal of its own copy (+1/−3).

⭐ Leg 9 — the fixture carries a DELIBERATE leak, so its invisibility is load-bearing

fork-build-inherits.fixture.ts contains a real fork(…, ['build'], { env: { ...process.env } }) — precisely what objectui#8598's gate exists to refuse. Both gates build their population with the same regex, and .fixture.ts matches neither ⇒ the planted leak cannot redden the live gate. ⚠️ And it does still have to type-check, because tsconfig.scripts.json compiles every .ts under scripts/ — ⭐ the dev named that tsconfig correctly and ran it, which is exactly the distinction errata 72 was paid for (type-check:scripts is a different command from per-package type-check).

⭐ Leg 10 is the whole card, and the report states its sharpest form

Against the live tree the change is unmeasurable: the fork( population is 0 (control spawnSync( = 35 in the same run, so the zero is a reading), so a run with fork in the set and a run without it are byte-identical and a green suite carries no information. Under ablation the two NEW cases fail on expected +0 to be 1 while the two pre-existing cases stay green.

⇒ ⭐ That is triage's 「绿色不携带任何信息」 converted from an argument into an observation. The count assertion is what does it: without it, 「nothing leaks」 and 「nothing was looked at」 are the same empty list.

Changeset — verified by CONTENT, ⛔ not by the cited line number

Empty frontmatter (--- immediately followed by ---). AGENTS.md, quoted verbatim from the head of this branch: 「纯内部改动/只动测试就写空 frontmatter(--- 紧跟 ---)显式声明"不发版",那是合法的一等通过写法」. ⚠️ The report cited it as "line 164"; the text is there but the line is not — ⭐ which is this shift's errata 74 in miniature: a line number is a measurement. Confirmed by content.

Status

CI on dd4a6d24fa: 26 success · 3 skipped · 6 in flight · ⛔ 0 red at 09:53Z. Still draft.
Next: green → draft→ready (⚠️ which ADDS Governed Surface Queue Guard, so the pre-flip green is stale) → re-wait for every check → enqueue → confirm added_to_merge_queue → ⭐ re-count all ten legs on the ACTUAL merge parent and publish the result whichever way it reads.

⛔ Nothing narrowed, no acceptance item unmet, no fence crossed. The one correction above is to the report's wording, ⛔ not to the work.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 13, 2026 09:59
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 80eb827 Sep 13, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-9211-spawners-single-source branch September 13, 2026 10:16
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#8598 gate's SPAWNERS set omits fork, which its objectui#8616 sibling includes

2 participants