Skip to content

test(cli): route the last three serve-* e2e spawners through childEnv(), 3 -> 0 on the child-env ratchet - #11692

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-11596-child-env-serve-slice
Aug 24, 2026
Merged

test(cli): route the last three serve-* e2e spawners through childEnv(), 3 -> 0 on the child-env ratchet#11692
os-zhuang merged 1 commit into
mainfrom
claude/issue-11596-child-env-serve-slice

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11596 — the final slice of the check:cli-test-child-env burn-down. Slice 1 (#11653)
took the cheap batch, 18 → 3; this one closes the ledger.

What changed

The three remaining bulk { ...process.env, … } child environments under packages/cli/test/**
stop building their spawned child out of the whole of the runner's environment:

- env: { ...process.env, NO_COLOR: '1' }
+ env: childEnv({ NO_COLOR: '1' })
filethe child it spawns
serve-app-anchored-optional-import.e2e.test.ts:160its hand-rolled runServeFrom() — a real os serve with the full stack, better-auth included
serve-app-runtime-hooks.e2e.test.ts:84os compile (its runServe() call was already on the choke point)
serve-host-fallback-base.e2e.test.ts:187tsx on a probe script that imports serve.ts and drives createHostImporter

childEnv() (#11267, packages/cli/test/helpers/serve-process.ts) copies the environment minus
the vitest worker family — TEST, VITEST, VITEST_* — and then applies the overrides.
scripts/cli-test-child-env.baseline.json loses its last three keys in the same commit; the
ratchet fails on a stale ceiling in that direction too, so leaving 3 there would silently
license three new leaks.

Measured, rather than asserted, on the recipe itself — parent seeded exactly as vitest seeds a
worker, same overrides, the only difference being the choke point:

BEFORE (bulk {...process.env, …}): {"TEST":"true","VITEST":"true","VITEST_WORKER_ID":"1","NO_COLOR":"1","OS_CLUSTER_DRIVER":"redis"}
AFTER (childEnv({ … }) ): {"NO_COLOR":"1","OS_CLUSTER_DRIVER":"redis"}

The red this slice was warned about, and what it actually was

The card predicted the serve-* children might need their environment re-established explicitly
— the OS_SECRET_KEY precedent. They did not. A different thing went red, and it is the
finding.

1. The gate's own self-test blocked the burn-down

pnpm check:cli-test-child-env runs --self-test first. With the three files repaired it
reported:

 ✗ the enumerated population contains serve-app-anchored-optional-import.e2e.test.ts
✗ the enumerated population contains serve-host-fallback-base.e2e.test.ts
✗ check-cli-test-child-env self-test: 2 of 54 case(s) failed.

Those two cases carry #11441's measurement, and their own comment states the intent: "they are
members of the population rather than extra scope"
. The predicate did not say that. It said
live.findings.some((f) => f.file === named) — membership of the findings list, which holds
only while those two files still LEAK. So it is an anti-shrink pin sitting on a shrink-only
ratchet: it passes on the day it is written, and reds the moment the burn-down it is filed
alongside does its job — reporting "the derivation lost this file" when the file is merely clean.
The ledger could not reach 0 while it stood.

Re-spelled to the claim the comment already makes: membership of the scanned population —
walked by walkSources, and classified by isSpawnerSource, so bulkEnvReferences really runs
over them. A repaired source is still derived and still scanned.

The gate's RULE is untouched: judge, bulkEnvReferences, countByFile and the DELIBERATE
registry are byte-identical, and no gate was widened.

Reverse-verified non-vacuous. Narrowing POPULATION from packages/cli/test/** to
packages/cli/test/helpers/** reds both cases again (5 of 54 case(s) failed). The mutation was
confirmed on disk before the run — grep -c on the old literal 1 → 0 and on the new one
0 → 1, plus a non-empty git diff --numstat — and the script carried
a trap on EXIT INT TERM restoring the file from a saved copy; after restore the old literal greps 1 again and the self-test
is back to 54 cases pass. No build is involved: the gate is an .mjs run directly.

2. The crypto re-check the card asked for: the mechanism it names no longer exists

The card, its slice-2 dispatch and helpers/serve-process.ts's header all predict that a child
which stops claiming to be a vitest worker flips local-crypto-provider's detectMode from
test to development, minting and persisting a key to $HOME/.objectstack/dev-crypto-key. The
header quotes the line at local-crypto-provider.ts:133:

if(env.VITEST||env.NODE_ENV==='test')return'test';

a58eac3e (#11448, merged 2026-08-23 22:36Z — before this card was even filed, 2026-08-24
07:25Z) deleted that arm.
detectMode now reads NODE_ENV and nothing else, and
pnpm check:runner-env-posture keeps the class shut. git grep 'VITEST' -- 'packages/**/src/**'
returns 6 hits, every one of them prose in that file's own header explaining the removal.

So stripping the runner family cannot move crypto posture here. And these children were never in
test posture to begin with: they run through bin/run-dev.js, which sets
process.env.NODE_ENV = 'development' before argv is parsed, and NODE_ENV is deliberately
outside childEnv()'s strip family. Confirmed empirically — $HOME/.objectstack/dev-crypto-key
was byte-identical and mtime-identical (md5 60a62ec9…, 2026-08-23 11:55:13) across every run
of all three files, before and after the repair: the boots settle on their marker before the
settings service mints anything.

No OS_SECRET_KEY was added. Adding one would have been the move the dispatch forbids in
reverse — a variable justified by nothing that was measured. The stale header is filed as
#11691 (finding, unassigned); it is a helper this slice was fenced out of editing.

Anti-vacuity: the leaks are gone, not moved

git grep -n '\.\.\.process\.env' packages/cli/test/ returns 11 lines. Ten are comment or prose
lines (the childEnv explanations the sweep left behind, plus the two measurement tables).
Exactly one code site survives:

packages/cli/test/serve-process-child-env.e2e.test.ts:114: return { ...process.env, ...OVERRIDES };

DELIBERATE, the pin leg that keeps the pre-repair recipe executable. The second DELIBERATE
site spells its bulk read Object.entries(process.env) (helpers/serve-process.ts:154), which a
literal-spread grep cannot match; --list reports it and it is untouched. That is exactly the two
DELIBERATE sites and nothing else.

A zero-hit grep certifies nothing on its own, so the same machinery ran for a term known to be
present: git grep -l 'childEnv(' packages/cli/test/ returns 24 files — 21 after slice 1,
plus these three.

--list, the source of truth rather than any hand-maintained worklist, agreed exactly with the
slice boundary before the edit and reports zero after:

before: 28 spawner source(s) in 85; 3 bulk copy/copies, 2 deliberate.
after: 28 spawner source(s) in 85; 0 bulk copy/copies, 2 deliberate.

Fences held

Why no changeset

Test-only under packages/cli/test/** plus two repo-root artifacts under scripts/. Neither
ships. Measured rather than assumed: @objectstack/cli declares
files: ["dist","README.md","CHANGELOG.md"], and npm pack --dry-run on the package yields 433
files of which 0 are under test/, 0 match *.e2e.test.* and 0 are under scripts/
— the same measurement slice 1 made, re-run here rather than cited. skip-changeset applies.

Clause ②: no. Test-only, no published surface, and no contract accept/reject behaviour
changes — the gate's rule (judge, bulkEnvReferences, DELIBERATE) is byte-identical and only
two of its self-test predicates moved.

Verification

Gate union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack,
which reports its own source: "gate list derived from the tree of 'objectstack-ai/objectstack' at
commit 5fdcae8"
and "change set derived from git — 5 path(s) vs merge base c251ef4". Derived
on the final committed diff, so the scripts/*.json and scripts/*.mjs families it pulls in are
included rather than missed by a first pass.

Everything below ran under scripts/pm/os-verify-lock.sh against the final commit 5fdcae87
on a clean tree, over a workspace built with
pnpm exec turbo run build --filter="./packages/*" --filter="./packages/*/*" (70/70 tasks
successful
) — several of these refuse outright on an unbuilt closure, and a refusal is not a
pass. Exit codes were captured before any pipe; each family is quoted from the verdict line the
gate itself prints. No log contains PREREQUISITE NOT MET, "Nothing was checked" or REFUSED.

All 20 families green:

✓ check:cli-test-child-env: 28 spawner source(s) among 85 under packages/cli/test/**; no new bulk
process.env copy reaches a spawned child (0 baselined in 0 file(s), ⛔ SHRINK-ONLY; 2 deliberate
site(s) still pinned).
✓ check-cli-test-child-env self-test: 54 cases pass
OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
(check:cross-package-test-inputs — 116 self-test cases; the ci.yml invocation is the same script)
✓ check:entry-guard: 142 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 100
export bindings, 90 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).
check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
check-i18n-coverage: OK (12 config(s), 657 baselined untranslated string(s), none new).
✓ check:parse-guard: 141 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
✓ check:pnpm-filter-targets: 120/148 `--filter` occurrence(s) across 25 file(s) resolve against 78
workspace package(s).
✓ check:published-files — 69 publishable package(s) of 78 workspace member(s) declare a `files`
whitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config
or build script.
✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new.
check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a
workspace dep through `dist/`.
check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the
DEBT ledger, 1 exempt.
check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 244.1s, 1897 raw
tsc error(s) total, none above its recorded number.
check-type-source-resolution OK — 77 packages with a tsconfig.json scanned.
✓ check:plugin-teardown-shape: 63 Plugin implementation(s) across 4591 source(s) under packages/**.
✓ affected-docs self-test: 395 cases pass. (docs-drift-check.yml)
✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new.
OK self-test (check:engine-double-contract) — no new engine double; this diff adds none.
✓ where-matcher conformance holds: 293 matcher(s) discovered, 293 answer the combinator battery
correctly or refuse it loudly (181 refuse).
✓ check-nul-bytes: OK (scanned 6519 text file(s); no raw ASCII control bytes).

check:type-check-debt's two surplus notices (plugin-approvals −1, runtime −1) are
pre-existing, in packages this diff does not touch, and the gate states they are not errors.

Tests

Each touched file, individually, at 5fdcae87 on a clean tree:

serve-app-anchored-optional-import.e2e.test.ts Test Files 1 passed (1) Tests 6 passed (6) 18.87s
serve-app-runtime-hooks.e2e.test.ts Test Files 1 passed (1) Tests 1 passed (1) 14.87s
serve-host-fallback-base.e2e.test.ts Test Files 1 passed (1) Tests 5 passed (5) 2.98s

The full packages/cli suite, sharded 4 ways (npx vitest run --shard=N/4 --maxWorkers=2; the
package's own config header prices the whole thing at ~495s):

shard 1/4 Test Files 43 passed (43) Tests 520 passed (520) 150.97s
shard 2/4 Test Files 43 passed (43) Tests 600 passed (600) 96.96s
shard 3/4 Test Files 42 passed (42) Tests 359 passed (359) 199.65s
shard 4/4 Test Files 42 passed (42) Tests 455 passed (455) 175.63s
------------------------------------------------------------
170 files, 1934 tests, 0 failed

pnpm --filter @objectstack/cli typechecktsc --noEmit, exit 0 (the script name is echoed in
the output, so this is not a zero-match --filter run).

Findings filed


Generated by Claude Code

… -> 0 on the child-env ratchet
Closes the `check:cli-test-child-env` burn-down. The three remaining bulk
`{ ...process.env, … }` child environments under `packages/cli/test/**` now go
through `childEnv()`, which strips the vitest worker family (TEST, VITEST,
VITEST_*) before applying overrides, and `scripts/cli-test-child-env.baseline.json`
loses its last three keys in the same commit.
The gate's own self-test had to be re-spelled to let the ratchet reach zero: two
cases pinned #11441's two files as members of `findings`, which holds only while
those files still LEAK. They are now pinned as members of the SCANNED population
instead -- walked and classified as a spawner -- which is the claim #11441
actually measured and the one that survives the repair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

ACCEPT — PM seat review (domain:cli, session 019siH5jDmk5hrayvfyojUqR, round R33)

CI at 5fdcae87: 22 of 31 complete, nothing red; the rest still running. Arming when they land.

⭐ The gate's own self-test had made its ledger un-burnable

This is the find of the slice, and it is structural rather than local:

two cases carrying #11441's measurement pinned those files as members of findings, a set that
holds only while they still LEAK, so repairing them reported 2 of 54 case(s) failed

A shrink-only ratchet whose self-test asserts the presence of its own entries can never reach
zero. The gate would have refused the last commit of the burn-down it exists to drive, and the
failure would have read as "the derivation lost this file" when the file was merely clean.

I checked the diff rather than the account, because "the dev edited a gate" is the shape that
should get scrutiny.
Only selfTest() changed: bulkEnvReferences, isSpawnerSource, the refusal
rules and the baseline comparison are byte-identical. The two cases moved from
live.findings.some((f) => f.file === named) to scanned.includes(named), where scanned re-walks
POPULATION_ROOTwith the gate's own helpers and filters to spawner sources.

Not 门禁削弱. The rule is unchanged, the case count is unchanged (54), and the old spelling
asserted a transient fact about the tree while the new one asserts a property of the gate — which
is what those cases' own comment already claimed in words ("they are members of the population").
And it still discriminates: narrowing the population to helpers/** reds it again, 5 of 54.

⚠️ One observation, not a blocker: scannedrecomputes the walk inside the self-test rather than
reading the gate's own enumerated population off live. Same helpers, so it is a re-invocation rather
than a second implementation — but a self-test that recomputes what it is checking can drift from the
thing it checks. If live ever carries the scanned set, read it from there.

The predicted red did not fire, and the reason is that the CARD was stale

I dispatched this expecting the crypto/auth re-check to bite and told you a red would be the
deliverable. It did not bite, and you established why rather than shrugging:

  • a58eac3e (fix(service-settings): select crypto posture from the deployment signal, never the test runner #11448) deleted the env.VITEST || arm of local-crypto-provider's detectMode,
    which now reads NODE_ENV alone. It merged 2026-08-23 22:36Zbefore this card was filed at
    07:25Z the next morning. So the card shipped with an already-stale mechanism, and neither triage nor
    this seat caught it.
  • These children run through bin/run-dev.js, which pins NODE_ENV=development before argv is
    parsed, so they were never in test crypto posture for childEnv() to move them out of.

⭐ And the negative was proved, not assumed: $HOME/.objectstack/dev-crypto-key byte- and
mtime-identical across every run ⇒ no OS_SECRET_KEY was added, because there was no measurement
to justify one.
That is the fence honoured in its hard direction — not adding a variable that would
have looked prudent and would have been cargo.

The best evidence in the report is the direct before/after

BEFORE {"TEST":"true","VITEST":"true","VITEST_WORKER_ID":"1","NO_COLOR":"1","OS_CLUSTER_DRIVER":"redis"}
AFTER {"NO_COLOR":"1","OS_CLUSTER_DRIVER":"redis"}

Parent seeded exactly as vitest seeds a worker. That shows the repair doing the thing, rather than
the gate agreeing that it did — the two are not the same claim and only one of them is direct.

Anti-vacuity holds too, with the undercount explained in advance: git grep '\.\.\.process\.env'
returns 11 lines, 10 prose, leaving one code site (serve-process-child-env.e2e.test.ts:114),
with the second DELIBERATE site's Object.entries(process.env) spelling reported by --list and
invisible to a literal grep. Reverse-check: childEnv( in 24 files — 21 after slice 1 plus these
three.

Ledger

0 baselined in 0 file(s), SHRINK-ONLY; 2 deliberate site(s) still pinned — key set emptied, and
this PR Fixes the card. #11595's form does not occur in these three (every spawn site carries
an env), measured and reported rather than assumed, so nothing was widened. #11691 filed for the
now-false detectMode quote in serve-process.ts's header — correctly a separate card, since this
slice was fenced out of that helper.

I am filing the class behind the self-test find separately: other shrink-only ratchets in this
repo may carry the same anti-shrink spelling, and that is not this card's to sweep.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 24, 2026 12:31
@os-zhuang
os-zhuang added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit c6a62d0Aug 24, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11596-child-env-serve-slice branch August 24, 2026 12:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

burn down the cli-test-child-env baseline: 18 spawned children in 17 packages/cli/test files still built from the whole of process.env

2 participants

@os-zhuang@claude