|
1 | 1 | """Tests for StreamableHTTPSessionManager."""
|
2 | 2 |
|
3 |
| -from unittest.mock import AsyncMock |
| 3 | +from unittest.mock import AsyncMock, patch |
4 | 4 |
|
5 | 5 | import anyio
|
6 | 6 | import pytest
|
7 | 7 |
|
| 8 | +from mcp.server import streamable_http_manager |
8 | 9 | from mcp.server.lowlevel import Server
|
9 | 10 | from mcp.server.streamable_http import MCP_SESSION_ID_HEADER
|
10 | 11 | from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
@@ -197,3 +198,65 @@ async def mock_receive():
|
197 | 198 | "Session ID should be removed from _server_instances after an exception"
|
198 | 199 | )
|
199 | 200 | assert not manager._server_instances, "No sessions should be tracked after the only session crashes"
|
| 201 | + |
| 202 | + |
| 203 | +@pytest.mark.anyio |
| 204 | +async def test_stateless_requests_memory_cleanup(): |
| 205 | + """Test that stateless requests actually clean up resources using real transports.""" |
| 206 | + app = Server("test-stateless-real-cleanup") |
| 207 | + manager = StreamableHTTPSessionManager(app=app, stateless=True) |
| 208 | + |
| 209 | + # Track created transport instances |
| 210 | + created_transports = [] |
| 211 | + |
| 212 | + # Patch StreamableHTTPServerTransport constructor to track instances |
| 213 | + |
| 214 | + original_constructor = streamable_http_manager.StreamableHTTPServerTransport |
| 215 | + |
| 216 | + def track_transport(*args, **kwargs): |
| 217 | + transport = original_constructor(*args, **kwargs) |
| 218 | + created_transports.append(transport) |
| 219 | + return transport |
| 220 | + |
| 221 | + with patch.object(streamable_http_manager, "StreamableHTTPServerTransport", side_effect=track_transport): |
| 222 | + async with manager.run(): |
| 223 | + # Mock app.run to complete immediately |
| 224 | + app.run = AsyncMock(return_value=None) |
| 225 | + |
| 226 | + # Send a simple request |
| 227 | + sent_messages = [] |
| 228 | + |
| 229 | + async def mock_send(message): |
| 230 | + sent_messages.append(message) |
| 231 | + |
| 232 | + scope = { |
| 233 | + "type": "http", |
| 234 | + "method": "POST", |
| 235 | + "path": "/mcp", |
| 236 | + "headers": [ |
| 237 | + (b"content-type", b"application/json"), |
| 238 | + (b"accept", b"application/json, text/event-stream"), |
| 239 | + ], |
| 240 | + } |
| 241 | + |
| 242 | + # Empty body to trigger early return |
| 243 | + async def mock_receive(): |
| 244 | + return { |
| 245 | + "type": "http.request", |
| 246 | + "body": b"", |
| 247 | + "more_body": False, |
| 248 | + } |
| 249 | + |
| 250 | + # Send a request |
| 251 | + await manager.handle_request(scope, mock_receive, mock_send) |
| 252 | + |
| 253 | + # Verify transport was created |
| 254 | + assert len(created_transports) == 1, "Should have created one transport" |
| 255 | + |
| 256 | + transport = created_transports[0] |
| 257 | + |
| 258 | + # The key assertion - transport should be terminated |
| 259 | + assert transport._terminated, "Transport should be terminated after stateless request" |
| 260 | + |
| 261 | + # Verify internal state is cleaned up |
| 262 | + assert len(transport._request_streams) == 0, "Transport should have no active request streams" |
0 commit comments