diff --git a/src/scenarios/server/lifecycle.test.ts b/src/scenarios/server/lifecycle.test.ts index ab3628a9..0610d980 100644 --- a/src/scenarios/server/lifecycle.test.ts +++ b/src/scenarios/server/lifecycle.test.ts @@ -27,10 +27,14 @@ describe('ServerInitializeScenario', () => { afterEach(() => { vi.unstubAllGlobals(); + vi.restoreAllMocks(); }); it('returns INFO when the server does not provide an MCP-Session-Id header', async () => { - fetchMock.mockResolvedValue(new Response(null)); + const cancelMock = vi.fn(); + fetchMock.mockResolvedValue( + new Response(new ReadableStream({ cancel: cancelMock })) + ); const checks = await new ServerInitializeScenario().run( testContext(serverUrl) @@ -45,9 +49,11 @@ describe('ServerInitializeScenario', () => { expect(fetchMock).toHaveBeenCalledWith( serverUrl, expect.objectContaining({ - method: 'POST' + method: 'POST', + signal: expect.any(AbortSignal) }) ); + expect(cancelMock).toHaveBeenCalled(); expect(checks).toHaveLength(2); expect(checks[0]?.id).toBe('server-initialize'); @@ -130,6 +136,25 @@ describe('ServerInitializeScenario', () => { ); }); + it('reports a failure when the raw session ID probe times out', async () => { + const probeSignal = new AbortController().signal; + vi.spyOn(AbortSignal, 'timeout').mockReturnValue(probeSignal); + fetchMock.mockRejectedValue(new Error('The operation was aborted')); + + const checks = await new ServerInitializeScenario().run( + testContext(serverUrl) + ); + + expect(AbortSignal.timeout).toHaveBeenCalledWith(5000); + expect(checks[1]).toMatchObject({ + id: 'server-session-id-visible-ascii', + status: 'FAILURE' + }); + expect(checks[1]?.errorMessage).toContain( + 'Failed to send initialize request for session ID check' + ); + }); + it('does not send DELETE when the server did not assign a session ID', async () => { fetchMock.mockResolvedValue(new Response(null)); diff --git a/src/scenarios/server/lifecycle.ts b/src/scenarios/server/lifecycle.ts index 7be721b1..38e25360 100644 --- a/src/scenarios/server/lifecycle.ts +++ b/src/scenarios/server/lifecycle.ts @@ -14,6 +14,7 @@ import { } from '../../connection/sdk-client'; const VISIBLE_ASCII_REGEX = /^[\x21-\x7E]+$/; +const SESSION_ID_PROBE_TIMEOUT_MS = 5_000; const SESSION_SPEC_REFERENCES = [ { @@ -115,7 +116,8 @@ and validates session ID format if one is assigned.`; version: '1.0.0' } } - }) + }), + signal: AbortSignal.timeout(SESSION_ID_PROBE_TIMEOUT_MS) }); const sessionId = response.headers.get('mcp-session-id'); @@ -164,6 +166,10 @@ and validates session ID format if one is assigned.`; } }); } + + // The probe only needs response headers. Release the body so a + // long-lived SSE response cannot keep a connection open. + await response.body?.cancel().catch(() => {}); } catch (error) { checks.push({ id: 'server-session-id-visible-ascii',