import asyncio import json from collections.abc import AsyncGenerator from typing import cast from dify_agent.protocol.schemas import RunFailedEvent, RunFailedEventData, RunStartedEvent from dify_agent.server.sse import format_sse_event, sse_event_stream def test_format_sse_event_uses_id_event_and_json_data() -> None: event = RunStartedEvent(id="7-0", run_id="run-1") frame = format_sse_event(event) assert frame.startswith("id: 7-0\nevent: run_started\ndata: ") assert '"run_id":"run-1"' in frame assert frame.endswith("\n\n") def test_format_sse_event_escapes_unicode_line_separators() -> None: error = "next-line:\x85line-separator:\u2028paragraph-separator:\u2029done" event = RunFailedEvent(id="8-0", run_id="run-1", data=RunFailedEventData(error=error)) frame = format_sse_event(event) data = next(line.removeprefix("data: ") for line in frame.splitlines() if line.startswith("data: ")) assert "\x85" not in frame assert "\u2028" not in frame assert "\u2029" not in frame assert "\\u0085" in frame assert "\\u2028" in frame assert "\\u2029" in frame assert json.loads(data)["data"]["error"] == error def test_sse_event_stream_emits_heartbeats_while_waiting() -> None: async def scenario() -> None: release = asyncio.Event() async def events(): await release.wait() yield RunStartedEvent(id="1-0", run_id="run-1") stream = cast(AsyncGenerator[str, None], sse_event_stream(events(), heartbeat_interval_seconds=0.001)) assert await anext(stream) == ": keepalive\n\n" _ = release.set() assert (await anext(stream)).startswith("id: 1-0\nevent: run_started") await stream.aclose() asyncio.run(scenario())