From 7d270b1dbc6f51e748a27f2fb96ea16e1f17881b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 01:55:14 +0000 Subject: [PATCH] test(cli): attribute the stdout-purity e2e probes to the serve child MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../serve-stdio-stdout-purity.e2e.test.ts | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts b/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts index a57da4bd1a..da773c2e7c 100644 --- a/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts +++ b/packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts @@ -48,6 +48,7 @@ import { E2E_SECRET_KEY, RUN_JS_RESOLVES_FROM_DIST, childEnv, + probeThroughChild, randomPort, requireBuiltCli, } from './helpers/serve-process.js'; @@ -357,22 +358,63 @@ describe('#7915: a stdio MCP boot writes nothing but protocol frames to stdout', // boot 2 sees the same row (`:memory:` would not survive the restart). const first = await boot({ OS_DATABASE_URL: join(dir, 'probe.db') }, /Server is ready/); const base = `http://localhost:${port}/api/v1`; - const signIn = await fetch(`${base}/auth/sign-in/email`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ email: 'admin@objectos.ai', password: 'admin123' }), + + // ⭐ #15898 — the fifth and last file of the family #15653 repaired. + // `boot()`'s `child.on('exit')` handler feeds the READINESS promise ONLY — it + // returns early once `settled` — so a death AFTER readiness is invisible to + // it, and the two requests below used to reach vitest as a bare + // `TypeError: fetch failed` with no exit code, no stdout and no stderr: the + // unattributable failure #15545 recorded once and could not diagnose. + // `probeThroughChild()` is where the child's fate is read instead; its own + // section in `helpers/serve-process.ts` carries the measurement, the bound + // and the fences (⛔ no skip, no quarantine, no timeout bump). + // + // Read at THROW time, so a failure carries what the child printed on its way + // down rather than the buffer as it stood at ready. + const transcript = () => + `\n--- child stdout ---\n${first.stdout()}\n--- child stderr ---\n${first.stderr()}`; + const probe = (what: string, request: () => Promise): Promise => + probeThroughChild( + { + child: first.child, + transcript, + label: 'serve-stdio-stdout-purity', + what: `${what} on port ${port}`, + }, + request, + ); + + // ⛔ The body read is INSIDE the thunk and cannot throw: a connection torn + // down mid-body rejects out of `res.json()` rather than out of `fetch()`, and + // the guard reads any throw as a transport failure — so an ASSERTION must + // never live in here or a wrong answer would be reported as a dropped socket. + const signIn = await probe('the sign-in probe', async () => { + const res = await fetch(`${base}/auth/sign-in/email`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + // `serve --dev` seeds this admin on an empty DB. + body: JSON.stringify({ email: 'admin@objectos.ai', password: 'admin123' }), + }); + let body: unknown = null; + try { body = await res.json(); } catch { /* non-JSON error body */ } + return { status: res.status, body }; }); expect(signIn.status).toBe(200); - const token = ((await signIn.json()) as { token?: string }).token; + const token = (signIn.body as { token?: string } | null)?.token; expect(token).toBeTruthy(); - const minted = await fetch(`${base}/keys`, { - method: 'POST', - headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify({ name: 'mcp-stdout-purity-e2e' }), + const minted = await probe('the key-mint probe', async () => { + const res = await fetch(`${base}/keys`, { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, + body: JSON.stringify({ name: 'mcp-stdout-purity-e2e' }), + }); + let body: unknown = null; + try { body = await res.json(); } catch { /* non-JSON error body */ } + return { status: res.status, body }; }); expect(minted.status).toBe(201); - apiKey = String(((await minted.json()) as { data: { key: string } }).data.key); + apiKey = String((minted.body as { data: { key: string } }).data.key); expect(apiKey.startsWith('osk_')).toBe(true); await stop(first.child);