修复生命周期锁阻塞事件循环、旧连接回调误停新连接及超时契约不一致。 补齐 MCP JSON 兼容导入、密钥拆分与失败重试,修复 Header 大小写草稿丢失,迁移大小写敏感的环境变量凭据。 在 SSE 行拼接前限制缓冲大小,增加并发、迁移和流式输入回归测试;忽略本机 MCP 数据及 server.json/servers.json。 验证:后端 185 项、前端 54 项测试通过,前端生产构建、相关文件 Ruff 与暂存差异检查通过。
84 lines
2.6 KiB
Python
84 lines
2.6 KiB
Python
import json
|
|
from contextlib import closing
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app.extensions import mcp
|
|
|
|
|
|
class ChunkStream(httpx.SyncByteStream):
|
|
def __init__(self, chunks):
|
|
self.chunks = chunks
|
|
self.bytes_read = 0
|
|
|
|
def __iter__(self):
|
|
for chunk in self.chunks:
|
|
self.bytes_read += len(chunk)
|
|
yield chunk
|
|
|
|
|
|
def test_sse_rejects_unterminated_line_before_reading_entire_stream(monkeypatch):
|
|
monkeypatch.setattr(mcp, "MAX_MCP_MESSAGE_BYTES", 1024)
|
|
stream = ChunkStream([b"x" * 256] * 256)
|
|
with (
|
|
closing(httpx.Response(200, stream=stream)) as response,
|
|
pytest.raises(mcp.McpBridgeError, match="too large"),
|
|
):
|
|
list(mcp._iter_sse(response))
|
|
assert stream.bytes_read == 1280
|
|
|
|
|
|
def test_sse_limits_combined_event_before_partial_line_is_complete(monkeypatch):
|
|
monkeypatch.setattr(mcp, "MAX_MCP_MESSAGE_BYTES", 32)
|
|
stream = ChunkStream(
|
|
[b"data: 123456789\n", b"data: 123456789\n", b"x", b"not-read"]
|
|
)
|
|
with (
|
|
closing(httpx.Response(200, stream=stream)) as response,
|
|
pytest.raises(mcp.McpBridgeError, match="too large"),
|
|
):
|
|
list(mcp._iter_sse(response))
|
|
assert stream.bytes_read == 33
|
|
|
|
|
|
@pytest.mark.parametrize("separator", [b"\n", b"\r", b"\r\n"])
|
|
@pytest.mark.parametrize("chunk_size", [1, 2, 7, 1024])
|
|
def test_sse_preserves_utf8_and_line_endings_across_chunks(separator, chunk_size):
|
|
payload = json.dumps(
|
|
{"jsonrpc": "2.0", "id": 1, "result": {"text": "中文"}}, ensure_ascii=False
|
|
)
|
|
wire = b"\xef\xbb\xbf" + separator.join(
|
|
[
|
|
b": heartbeat",
|
|
b"event: message",
|
|
b"id: replay-1",
|
|
("data: " + payload).encode(),
|
|
b"",
|
|
b"",
|
|
]
|
|
)
|
|
stream = ChunkStream(
|
|
[wire[index : index + chunk_size] for index in range(0, len(wire), chunk_size)]
|
|
)
|
|
with closing(httpx.Response(200, stream=stream)) as response:
|
|
assert list(mcp._iter_sse(response)) == [("message", "replay-1", payload)]
|
|
|
|
|
|
def test_sse_event_limit_resets_between_events(monkeypatch):
|
|
monkeypatch.setattr(mcp, "MAX_MCP_MESSAGE_BYTES", 16)
|
|
with closing(
|
|
httpx.Response(200, stream=ChunkStream([b"data: one\n\ndata: two\r\r"]))
|
|
) as response:
|
|
assert list(mcp._iter_sse(response)) == [
|
|
("message", None, "one"),
|
|
("message", None, "two"),
|
|
]
|
|
|
|
|
|
def test_sse_preserves_multiline_data_and_final_unterminated_line():
|
|
with closing(
|
|
httpx.Response(200, stream=ChunkStream([b"data: first\ndata: last"]))
|
|
) as response:
|
|
assert list(mcp._iter_sse(response)) == [("message", None, "first\nlast")]
|