test(cli): attribute the stdout-purity e2e probes to the serve child - #16462
Merged
Merged
Conversation
`serve-stdio-stdout-purity.e2e.test.ts` was the fifth and last file in the
family that fetched a spawned `os serve` with no child-lifecycle attribution.
Its `boot()` handler for `child.on('exit')` returns early on `if (settled)
return`, so it feeds the readiness promise only: a death after readiness was
invisible to it, and the two `beforeAll` probes reached vitest as a bare
`TypeError: fetch failed` with no exit code, no stdout and no stderr.
Route both probes — `POST auth/sign-in/email` and `POST keys` — through the
existing `probeThroughChild()` helper, the same shape the four siblings already
use. The body read goes inside the thunk (a connection torn down mid-body
rejects out of `res.json()`, not out of `fetch()`) and the assertions stay
outside it (the guard reads any throw as a transport failure).
No product change and no new export: test-only, through a helper already on
`main` with its own pin at `serve-probe-child-attribution.test.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Contributor
📓 Docs Drift CheckNothing 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. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-litant
marked this pull request as ready for review
September 7, 2026 03:38
os-litant
enabled auto-merge
September 7, 2026 03:38
os-litant
deleted the
claude/issue-15898-stdio-purity-child-attribution
branch
September 7, 2026 04:04
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 free
to 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 #15898
serve-stdio-stdout-purity.e2e.test.tswas the fifth and last file in the family that fetched a spawnedos servewith no child-lifecycle attribution. Itsboot()handler forchild.on('exit')returns early onif (settled) return, so it feeds the READINESS promise only — a death after readiness was invisible to it, and the twobeforeAllprobes reached vitest as a bareTypeError: fetch failedwith no exit code, no stdout and no stderr. This file runs onTest Core, a required shard, so it was the worst place to hold a red nobody could explain.Both probes now route through the existing
probeThroughChild()helper, the same shape the four siblings already use. No redesign, no new export, no sixth file.Pre-state, measured on this tree rather than inherited
The card's tense was stale (it wrote the helper as "landing"); the helper is on
mainatpackages/cli/test/helpers/serve-process.ts:1117. Re-measured here before editing:probeThroughChildoccurrences in the target file:360POST auth/sign-in/email,:369POST keysif (settled) returnearly-returns feeding only the readiness promise:200(timeout),:210(output),:239(exit)Matched on
fetch(, notawait fetch(— the card's own correction about its recipe's reach. That distinction is between a blind tool and a wrong conclusion: the card's reasoning was sound, only its grep under-reached. Here both sites happened to be spelledawait fetch(, so the wider pattern changed no count in this file — it was used because the narrower one cannot be trusted to, not because it moved the number.serve-port-drift-notice.e2e.test.tsremains untouched: it fetches the neighbour fixture it owns atneighbour.port, never the spawned child.The repair is demonstrated, not asserted
A conversion nobody demonstrated is a refactor. The prediction below was written to disk before either leg ran; both legs kill the child through the handle the file itself owns (never by name pattern — the process table is shared).
The mutation is identical in both legs:
SIGKILLthe child immediately afterconst base = ..., i.e. after readiness has settled — precisely the windowif (settled) returnmakes invisible.fetch)TypeError: fetch failed, causeECONNREFUSED, naming no exit code, no signal, no stdout, no stderrTypeError: fetch failed/Caused by: Error: connect ECONNREFUSED 127.0.0.1:41851. Grep for exit code, signal or transcript: 0 hitsprobeThroughChild)os serve DIED while answering the sign-in probe on port N — exit code null, signal SIGKILL, not retried, transcript attachedos serve DIED while answering the sign-in probe on port 39907 — exit code null, signal SIGKILL. This is the CHILD's failure, not a dropped socket, so it is NOT retriedattempt 1/3— one onlyexit code nullwithsignal SIGKILLis the honest reading of a signalled death —fateOf()reports the signal where there is no numeric code; a child that exits on its own reports the number instead. Both are attribution, which is the thing that was missing.One place the observation was sharper than the prediction: the prediction expected the boot banner in the
--- child stdout ---half. Observed, that half is empty and the whole boot log arrives on--- child stderr ---. That is not a shortfall — it is this file's own invariant confirmed in passing:serve-stdio-stdout-purityexists to pin that a healthy boot puts nothing but protocol frames on stdout.Ablation hygiene: absolute-path
trap ... EXIT INT TERM; the mutation was proven on disk by grepping the injected text (never the editor's exit code); each restore was proven bygit hash-objectequal to theHEADblob and an emptygit diff HEAD, and spelledgit checkout HEAD -- pathrather than the bare form, which restores from the index the other leg had polluted. The subject is the test file and a same-tree relative helper import, both read from source by vitest, so nodistleg exists to go stale.Verification
vitest run test/serve-stdio-stdout-purity.e2e.test.ts— 1 file, 1 test passed, 10.32s.test/serve-probe-child-attribution.test.ts— 7 passed.pnpm --filter @objectstack/cli typecheckgreen. The package'stsconfig.jsonincludes onlysrc, sotsc --noEmitsays nothing about this file; coverage was proven separately with--listFilesontsconfig.test.json(the edited file and the helper both present). Raw diagnostics: 28 across 3 files, all pre-existing and ledgered, 0 attributable to this diff.dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each exit code captured before any pipe. 43 measured green. 1 NOT MEASURED, not a pass:check:dual-build-cjs-loadsexits 3PREREQUISITE NOT METbecause 11 packages have nodist/in this worktree (only the@objectstack/cliclosure was built). It reads built output of published packages and this diff changes no package source.check:type-check-debtfirst exited 3 on a V8 OOM on the shared box; re-run with a larger heap it is measured and green (5 ledger entries, 55 raw errors, none above its recorded number).--format json(1 file, 0 errors, 0 warnings), and the config enables no type-aware linting anywhere (0parserOptions.project, 0projectService), so this diff cannot move the verdict on any file it did not touch.Clause 2 — graded
noNo newly exported symbol, no new key on any published payload, no
packages/spec/src/**path. Test-only, through a helper that already exists with an unchanged signature. The label is therefore not hung here.No changeset: this publishes nothing from any package, so it carries
skip-changeset.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code