"""SSE parser tests — the wire-format glue. Streamable-HTTP is what the FastMCP server speaks. Our parser turns a short SSE body into a JSON-RPC envelope. Edge cases worth testing: * single ``data:`` line (the common case) * multiple ``data:`` lines that, joined, form the JSON * leading ``event:`` / ``id:`` lines that must be ignored * ``:`` comment lines must be ignored * empty body must raise * ``data:`` with non-JSON must raise * missing ``data:`` line must raise """ from __future__ import annotations import json import pytest from helix_cli.mcp_client import SseParseError, parse_sse_payload def test_single_data_line() -> None: body = 'event: message\ndata: {"jsonrpc":"2.0","id":1,"result":{"ok":true}}\n\n' parsed = parse_sse_payload(body) assert parsed["result"]["ok"] is True def test_multi_data_lines_concat() -> None: """Per SSE spec, multiple ``data:`` lines join with ``\\n`` to form the message payload. We must support this even though FastMCP's current emitter typically uses a single line. """ body = 'data: {"jsonrpc":"2.0",\ndata: "id":1,\ndata: "result":{}}\n\n' parsed = parse_sse_payload(body) assert parsed["jsonrpc"] == "2.0" assert parsed["id"] == 1 def test_jsonrpc_error_round_trip() -> None: """An MCP server-side error envelope round-trips fine — the parser only cares about producing a JSON object; classifying error vs success is the caller's job. """ body = 'data: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"bad params"}}\n\n' parsed = parse_sse_payload(body) assert parsed["error"]["code"] == -32602 def test_empty_body_raises() -> None: with pytest.raises(SseParseError, match="empty"): parse_sse_payload("") def test_no_data_line_raises() -> None: body = "event: message\n: just a comment\nid: 99\n\n" with pytest.raises(SseParseError, match="no `data:` line"): parse_sse_payload(body) def test_non_json_data_raises() -> None: body = "data: not actually json\n\n" with pytest.raises(SseParseError, match="not valid JSON"): parse_sse_payload(body) def test_comments_and_other_fields_ignored() -> None: body = ( ": this is an SSE comment and must be skipped\n" "event: message\n" "id: 7\n" "retry: 1000\n" 'data: {"jsonrpc":"2.0","id":1,"result":42}\n' "\n" ) parsed = parse_sse_payload(body) assert parsed["result"] == 42 def test_leading_space_after_data_colon_stripped() -> None: """Per SSE spec: a single space after the field colon must be stripped from the value. ``data:{"k":1}`` and ``data: {"k":1}`` must produce the same JSON. """ body_with_space = 'data: {"k":1}\n\n' body_without_space = 'data:{"k":1}\n\n' assert parse_sse_payload(body_with_space) == parse_sse_payload(body_without_space) def test_only_first_message_consumed() -> None: """Streamable-HTTP single-shot sends one logical message. Even if a buggy server sends two, we read the first cleanly. """ body = ( 'data: {"jsonrpc":"2.0","id":1,"result":1}\n\n' 'data: {"jsonrpc":"2.0","id":2,"result":2}\n\n' ) parsed = parse_sse_payload(body) assert parsed["result"] == 1 def test_non_dict_payload_raises() -> None: body = "data: [1,2,3]\n\n" with pytest.raises(SseParseError, match="JSON object"): parse_sse_payload(body) def test_real_world_shape_from_test_helper() -> None: """Sanity-check we match the regex used in the existing test helper at tests/test_auth/test_mcp_endpoint.py — i.e. the parser handles real bodies the live server returns. """ body = ( 'event: message\n' 'data: {"jsonrpc":"2.0","id":1,"result":' '{"protocolVersion":"2025-06-18","serverInfo":{"name":"helix"},' '"capabilities":{"tools":{}}}}\n\n' ) parsed = parse_sse_payload(body) assert parsed["result"]["serverInfo"]["name"] == "helix" assert "tools" in parsed["result"]["capabilities"] # And it round-trips cleanly through json.dumps for our --json mode json.dumps(parsed)