build: fail the release when smoke tests fail, and fix the SSE test that hung
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m6s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 4m40s
CI/CD / publish (pull_request) Has been skipped

release.sh printed `WARNING: Smoke tests failed!` and exited 0. That is how four
broken readiness probes shipped unnoticed on 2026-08-10 — the warning scrolled
past in the build log and the release reported success.

It now exits 1, with `MCPCTL_ALLOW_SMOKE_FAILURE=1` as the escape hatch. The
message is explicit that smoke runs LAST, against the installed binary: the
package is already published and installed, so the failure reports fleet
breakage rather than preventing a bad artifact.

Turning the gate on required fixing a latent hang first, or every release would
have blocked on it. `security.test.ts > /inspect SSE endpoint …` waited for a
response body that by design never ends, so it could only settle via the
socket's *inactivity* timeout — and /inspect relays every project's MCP traffic,
so during a full smoke run it is never idle. Run alone it passed and looked
flaky; run with the suite it failed every time.

httpRequest gains `headersOnly`, which resolves on the response headers and
hangs up. The assertion only ever needed the status line.

Verified: full smoke suite 158/158 (was 157/158 with this test timing out); the
gate block lifted verbatim from release.sh exits 1 with a stubbed failing smoke
run, and exits 0 reaching subsequent code under MCPCTL_ALLOW_SMOKE_FAILURE=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wUmrfkVQR6CKcYKxENq7k
This commit is contained in:
Michal
2026-08-10 17:15:40 +01:00
parent cd94e855aa
commit 822c1bb047
3 changed files with 52 additions and 8 deletions

View File

@@ -32,6 +32,18 @@ function httpRequest(opts: {
headers?: Record<string, string>;
body?: string;
timeout?: number;
/**
* Resolve as soon as the response headers arrive, then hang up, instead of
* waiting for the body to end.
*
* Required for a streaming endpoint: SSE responses never end, so the normal
* path can only settle via the socket's *inactivity* timeout — which never
* fires while the stream is busy. `/inspect` relays every project's MCP
* traffic, so during a full smoke run it is never idle, and the request hung
* until vitest killed the test. Alone it looked flaky; under load it failed
* every time. Reading the status does not need the body anyway.
*/
headersOnly?: boolean;
}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
return new Promise((resolve, reject) => {
const parsed = new URL(opts.url);
@@ -46,6 +58,12 @@ function httpRequest(opts: {
timeout: opts.timeout ?? 10_000,
},
(res) => {
if (opts.headersOnly === true) {
resolve({ status: res.statusCode ?? 0, headers: res.headers, body: '' });
res.destroy();
req.destroy();
return;
}
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
@@ -93,17 +111,15 @@ describe('Smoke: Security — mcplocal unauthenticated endpoints', () => {
// /inspect streams ALL MCP traffic (tool calls, arguments, responses)
// for ALL projects to any unauthenticated local client
// headersOnly: the stream never ends, and waiting for it to go idle is what
// made this hang whenever other suites were generating traffic. The status
// line is all this assertion needs.
const res = await httpRequest({
url: `${MCPLOCAL_URL}/inspect`,
method: 'GET',
headers: { 'Accept': 'text/event-stream' },
timeout: 3_000,
}).catch((err) => {
// Timeout is expected (SSE keeps connection open) — still means endpoint is accessible
if ((err as Error).message.includes('timed out')) {
return { status: 200, headers: {} as http.IncomingHttpHeaders, body: '' };
}
throw err;
headersOnly: true,
});
// Should be accessible without auth (documenting the vulnerability)