Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
E2E_SECRET_KEY,
RUN_JS_RESOLVES_FROM_DIST,
childEnv,
probeThroughChild,
randomPort,
requireBuiltCli,
} from './helpers/serve-process.js';
Expand Down Expand Up @@ -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 = <T>(what: string, request: () => Promise<T>): Promise<T> =>
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);
Expand Down
Loading