Skip to content

Commit 2228859

Browse files
os-litantclaude
andauthored
test(cli): attribute the stdout-purity e2e probes to the serve child (#16462)
`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`. Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N Co-authored-by: Claude <noreply@anthropic.com>
1 parent 51babcf commit 2228859

1 file changed

Lines changed: 52 additions & 10 deletions

File tree

‎packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts‎

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
E2E_SECRET_KEY,
4949
RUN_JS_RESOLVES_FROM_DIST,
5050
childEnv,
51+
probeThroughChild,
5152
randomPort,
5253
requireBuiltCli,
5354
} from './helpers/serve-process.js';
@@ -357,22 +358,63 @@ describe('#7915: a stdio MCP boot writes nothing but protocol frames to stdout',
357358
// boot 2 sees the same row (`:memory:` would not survive the restart).
358359
const first = await boot({ OS_DATABASE_URL: join(dir, 'probe.db') }, /Server is ready/);
359360
const base = `http://localhost:${port}/api/v1`;
360-
const signIn = await fetch(`${base}/auth/sign-in/email`, {
361-
method: 'POST',
362-
headers: { 'content-type': 'application/json' },
363-
body: JSON.stringify({ email: 'admin@objectos.ai', password: 'admin123' }),
361+
362+
// ⭐ #15898 — the fifth and last file of the family #15653 repaired.
363+
// `boot()`'s `child.on('exit')` handler feeds the READINESS promise ONLY — it
364+
// returns early once `settled` — so a death AFTER readiness is invisible to
365+
// it, and the two requests below used to reach vitest as a bare
366+
// `TypeError: fetch failed` with no exit code, no stdout and no stderr: the
367+
// unattributable failure #15545 recorded once and could not diagnose.
368+
// `probeThroughChild()` is where the child's fate is read instead; its own
369+
// section in `helpers/serve-process.ts` carries the measurement, the bound
370+
// and the fences (⛔ no skip, no quarantine, no timeout bump).
371+
//
372+
// Read at THROW time, so a failure carries what the child printed on its way
373+
// down rather than the buffer as it stood at ready.
374+
const transcript = () =>
375+
`\n--- child stdout ---\n${first.stdout()}\n--- child stderr ---\n${first.stderr()}`;
376+
const probe = <T>(what: string, request: () => Promise<T>): Promise<T> =>
377+
probeThroughChild(
378+
{
379+
child: first.child,
380+
transcript,
381+
label: 'serve-stdio-stdout-purity',
382+
what: `${what} on port ${port}`,
383+
},
384+
request,
385+
);
386+
387+
// ⛔ The body read is INSIDE the thunk and cannot throw: a connection torn
388+
// down mid-body rejects out of `res.json()` rather than out of `fetch()`, and
389+
// the guard reads any throw as a transport failure — so an ASSERTION must
390+
// never live in here or a wrong answer would be reported as a dropped socket.
391+
const signIn = await probe('the sign-in probe', async () => {
392+
const res = await fetch(`${base}/auth/sign-in/email`, {
393+
method: 'POST',
394+
headers: { 'content-type': 'application/json' },
395+
// `serve --dev` seeds this admin on an empty DB.
396+
body: JSON.stringify({ email: 'admin@objectos.ai', password: 'admin123' }),
397+
});
398+
let body: unknown = null;
399+
try { body = await res.json(); } catch { /* non-JSON error body */ }
400+
return { status: res.status, body };
364401
});
365402
expect(signIn.status).toBe(200);
366-
const token = ((await signIn.json()) as { token?: string }).token;
403+
const token = (signIn.body as { token?: string } | null)?.token;
367404
expect(token).toBeTruthy();
368405

369-
const minted = await fetch(`${base}/keys`, {
370-
method: 'POST',
371-
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
372-
body: JSON.stringify({ name: 'mcp-stdout-purity-e2e' }),
406+
const minted = await probe('the key-mint probe', async () => {
407+
const res = await fetch(`${base}/keys`, {
408+
method: 'POST',
409+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
410+
body: JSON.stringify({ name: 'mcp-stdout-purity-e2e' }),
411+
});
412+
let body: unknown = null;
413+
try { body = await res.json(); } catch { /* non-JSON error body */ }
414+
return { status: res.status, body };
373415
});
374416
expect(minted.status).toBe(201);
375-
apiKey = String(((await minted.json()) as { data: { key: string } }).data.key);
417+
apiKey = String((minted.body as { data: { key: string } }).data.key);
376418
expect(apiKey.startsWith('osk_')).toBe(true);
377419

378420
await stop(first.child);

0 commit comments

Comments
 (0)