"""Unit: SSE frame serialization for POST /api/chat (PLAN §4 contract).""" from __future__ import annotations import json from app.api.chat import sse_event def _payload(frame: str) -> dict: assert frame.startswith("data: ") assert frame.endswith("\n\n") return json.loads(frame.removeprefix("data: ").strip()) def test_delta_frame_serializes_exactly() -> None: frame = sse_event({"type": "delta", "text": "hi"}) assert frame == 'data: {"type": "delta", "text": "hi"}\n\n' assert _payload(frame) == {"type": "delta", "text": "hi"} def test_done_frame_carries_full_contract_shape() -> None: payload = { "type": "done", "deflected": False, "sources": [{"source": "Homelab", "path": "kubernetes.md", "title": "K8s"}], "suggestions": [], } assert _payload(sse_event(payload)) == payload def test_error_frame_serializes() -> None: frame = sse_event({"type": "error", "detail": "boom"}) assert _payload(frame) == {"type": "error", "detail": "boom"} def test_unicode_survives_roundtrip() -> None: frame = sse_event({"type": "delta", "text": "🧠 café — \"quoted\""}) # ensure_ascii=False keeps the frame readable (no \uXXXX escapes). assert "🧠 café" in frame assert _payload(frame)["text"] == "🧠 café — \"quoted\"" def test_multi_line_text_stays_one_frame() -> None: """Newlines inside the JSON payload must be escaped so the frame delimiter ``\\n\\n`` remains unambiguous.""" frame = sse_event({"type": "delta", "text": "line1\nline2\n\n"}) assert frame.count("\n\n") == 1 # only the frame terminator assert _payload(frame)["text"] == "line1\nline2\n\n"