1use a2a_protocol_types::{JsonRpcResponse, StreamResponse};
35use hyper::body::Bytes;
36use tokio::sync::mpsc;
37use tokio::task::AbortHandle;
38
39use crate::error::{ClientError, ClientResult};
40use crate::streaming::sse_parser::SseParser;
41
42pub(crate) type BodyChunk = ClientResult<Bytes>;
46
47const EVENT_BRIDGE_CAPACITY: usize = 64;
53
54pub struct EventStream {
65 rx: mpsc::Receiver<BodyChunk>,
67 parser: SseParser,
69 done: bool,
71 abort_handle: Option<AbortHandle>,
73 status_code: u16,
80 jsonrpc_envelope: bool,
86 first_event_timeout: Option<std::time::Duration>,
94 first_chunk_received: bool,
96}
97
98impl EventStream {
99 #[must_use]
105 #[cfg(any(test, feature = "websocket"))]
106 pub(crate) fn new(rx: mpsc::Receiver<BodyChunk>) -> Self {
107 Self {
108 rx,
109 parser: SseParser::new(),
110 done: false,
111 abort_handle: None,
112 status_code: 200,
113 jsonrpc_envelope: true,
114 first_event_timeout: None,
115 first_chunk_received: false,
116 }
117 }
118
119 #[must_use]
124 #[cfg(test)]
125 pub(crate) fn with_abort_handle(
126 rx: mpsc::Receiver<BodyChunk>,
127 abort_handle: AbortHandle,
128 ) -> Self {
129 Self {
130 rx,
131 parser: SseParser::new(),
132 done: false,
133 abort_handle: Some(abort_handle),
134 status_code: 200,
135 jsonrpc_envelope: true,
136 first_event_timeout: None,
137 first_chunk_received: false,
138 }
139 }
140
141 #[must_use]
173 pub fn from_event_channel(mut rx: mpsc::Receiver<ClientResult<StreamResponse>>) -> Self {
174 let (tx, body_rx) = mpsc::channel::<BodyChunk>(EVENT_BRIDGE_CAPACITY);
175
176 let bridge = tokio::spawn(async move {
182 while let Some(event) = rx.recv().await {
183 let chunk = match event {
184 Ok(ref ev) => serde_json::to_string(ev).map_or_else(
185 |e| Err(ClientError::Serialization(e)),
186 |json| {
187 Ok(Bytes::from(format!(
188 "data: {{\"jsonrpc\":\"2.0\",\"id\":null,\"result\":{json}}}\n\n"
189 )))
190 },
191 ),
192 Err(e) => Err(e),
193 };
194 if tx.send(chunk).await.is_err() {
195 break;
196 }
197 }
198 });
199
200 Self::with_status(body_rx, bridge.abort_handle(), 200)
201 }
202
203 #[must_use]
206 pub(crate) fn with_status(
207 rx: mpsc::Receiver<BodyChunk>,
208 abort_handle: AbortHandle,
209 status_code: u16,
210 ) -> Self {
211 Self {
212 rx,
213 parser: SseParser::new(),
214 done: false,
215 abort_handle: Some(abort_handle),
216 status_code,
217 jsonrpc_envelope: true,
218 first_event_timeout: None,
219 first_chunk_received: false,
220 }
221 }
222
223 #[must_use]
228 pub(crate) const fn with_jsonrpc_envelope(mut self, envelope: bool) -> Self {
229 self.jsonrpc_envelope = envelope;
230 self
231 }
232
233 #[must_use]
244 pub(crate) const fn with_first_event_timeout(mut self, timeout: std::time::Duration) -> Self {
245 self.first_event_timeout = Some(timeout);
246 self
247 }
248
249 #[must_use]
254 pub const fn status_code(&self) -> u16 {
255 self.status_code
256 }
257
258 pub async fn next(&mut self) -> Option<ClientResult<StreamResponse>> {
265 loop {
266 if let Some(result) = self.parser.next_frame() {
268 match result {
269 Ok(frame) => return Some(self.decode_frame(&frame.data)),
270 Err(e) => {
271 return Some(Err(ClientError::Transport(e.to_string())));
272 }
273 }
274 }
275
276 if self.done {
277 return None;
278 }
279
280 let chunk = match self.first_event_timeout {
285 Some(timeout) if !self.first_chunk_received => {
286 let Ok(chunk) = tokio::time::timeout(timeout, self.rx.recv()).await else {
287 self.done = true;
288 return Some(Err(ClientError::Timeout(
289 "stream produced no data before the first-event timeout".into(),
290 )));
291 };
292 chunk
293 }
294 _ => self.rx.recv().await,
295 };
296 match chunk {
297 None => {
298 self.done = true;
300 if let Some(result) = self.parser.next_frame() {
302 match result {
303 Ok(frame) => return Some(self.decode_frame(&frame.data)),
304 Err(e) => {
305 return Some(Err(ClientError::Transport(e.to_string())));
306 }
307 }
308 }
309 return None;
310 }
311 Some(Err(e)) => {
312 self.done = true;
313 return Some(Err(e));
314 }
315 Some(Ok(bytes)) => {
316 self.first_chunk_received = true;
317 self.parser.feed(&bytes);
318 }
319 }
320 }
321 }
322
323 fn decode_frame(&mut self, data: &str) -> ClientResult<StreamResponse> {
326 if self.jsonrpc_envelope {
327 let envelope: JsonRpcResponse<StreamResponse> =
329 serde_json::from_str(data).map_err(ClientError::Serialization)?;
330
331 match envelope {
332 JsonRpcResponse::Success(ok) => {
333 if is_terminal(&ok.result) {
334 self.done = true;
335 }
336 Ok(ok.result)
337 }
338 JsonRpcResponse::Error(err) => {
339 self.done = true;
340 let a2a = crate::transport::map_jsonrpc_error(
341 err.error.code,
342 err.error.message,
343 err.error.data,
344 );
345 Err(ClientError::Protocol(a2a))
346 }
347 }
348 } else {
349 let event: StreamResponse =
352 serde_json::from_str(data).map_err(ClientError::Serialization)?;
353 if is_terminal(&event) {
354 self.done = true;
355 }
356 Ok(event)
357 }
358 }
359}
360
361impl Drop for EventStream {
362 fn drop(&mut self) {
363 if let Some(handle) = self.abort_handle.take() {
364 handle.abort();
365 }
366 }
367}
368
369#[allow(clippy::missing_fields_in_debug)]
370impl std::fmt::Debug for EventStream {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 f.debug_struct("EventStream")
374 .field("done", &self.done)
375 .field("pending_frames", &self.parser.pending_count())
376 .finish()
377 }
378}
379
380const fn is_terminal(event: &StreamResponse) -> bool {
382 matches!(
383 event,
384 StreamResponse::StatusUpdate(ev) if ev.status.state.is_terminal()
385 )
386}
387
388#[cfg(test)]
391mod tests {
392 use super::*;
393 use a2a_protocol_types::{
394 JsonRpcSuccessResponse, JsonRpcVersion, TaskId, TaskState, TaskStatus,
395 TaskStatusUpdateEvent,
396 };
397 use std::time::Duration;
398
399 const TEST_TIMEOUT: Duration = Duration::from_secs(5);
402
403 fn make_status_event(state: TaskState, _is_final: bool) -> StreamResponse {
404 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
405 task_id: TaskId::new("t1"),
406 context_id: a2a_protocol_types::ContextId::new("c1"),
407 status: TaskStatus {
408 state,
409 message: None,
410 timestamp: None,
411 },
412 metadata: None,
413 })
414 }
415
416 fn sse_frame(event: &StreamResponse) -> String {
417 let resp = JsonRpcSuccessResponse {
418 jsonrpc: JsonRpcVersion,
419 id: Some(serde_json::json!(1)),
420 result: event.clone(),
421 };
422 let json = serde_json::to_string(&resp).unwrap();
423 format!("data: {json}\n\n")
424 }
425
426 #[tokio::test]
427 async fn stream_delivers_events() {
428 let (tx, rx) = mpsc::channel(8);
429 let mut stream = EventStream::new(rx);
430
431 let event = make_status_event(TaskState::Working, false);
432 let sse_bytes = sse_frame(&event);
433 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
434 drop(tx);
435
436 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
437 .await
438 .expect("timed out")
439 .unwrap()
440 .unwrap();
441 assert!(
442 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
443 );
444 }
445
446 #[tokio::test]
447 async fn stream_ends_on_final_event() {
448 let (tx, rx) = mpsc::channel(8);
449 let mut stream = EventStream::new(rx);
450
451 let event = make_status_event(TaskState::Completed, true);
452 let sse_bytes = sse_frame(&event);
453 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
454
455 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
457 .await
458 .expect("timed out waiting for final event")
459 .unwrap()
460 .unwrap();
461 assert!(
462 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
463 );
464
465 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
467 .await
468 .expect("timed out waiting for stream end");
469 assert!(end.is_none());
470 }
471
472 #[tokio::test]
473 async fn stream_propagates_body_error() {
474 let (tx, rx) = mpsc::channel(8);
475 let mut stream = EventStream::new(rx);
476
477 tx.send(Err(ClientError::Transport("network error".into())))
478 .await
479 .unwrap();
480
481 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
482 .await
483 .expect("timed out")
484 .unwrap();
485 assert!(result.is_err());
486 }
487
488 #[tokio::test]
489 async fn stream_ends_when_channel_closed() {
490 let (tx, rx) = mpsc::channel(8);
491 let mut stream = EventStream::new(rx);
492 drop(tx);
493
494 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
495 .await
496 .expect("timed out");
497 assert!(result.is_none());
498 }
499
500 #[tokio::test]
508 async fn from_event_channel_delivers_events() {
509 let (tx, rx) = mpsc::channel(8);
510 let mut stream = EventStream::from_event_channel(rx);
511
512 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
513 task_id: TaskId::new("task-1"),
514 context_id: a2a_protocol_types::ContextId::new("ctx-1"),
515 status: TaskStatus::new(TaskState::Working),
516 metadata: None,
517 });
518 tx.send(Ok(event)).await.expect("send");
519 drop(tx);
520
521 let received = tokio::time::timeout(TEST_TIMEOUT, stream.next())
522 .await
523 .expect("timed out")
524 .expect("a sent event must arrive")
525 .expect("and must not be an error");
526
527 match received {
528 StreamResponse::StatusUpdate(ev) => {
529 assert_eq!(ev.task_id, TaskId::new("task-1"));
530 assert_eq!(ev.status.state, TaskState::Working);
531 }
532 other => panic!("expected a status update, got {other:?}"),
533 }
534 }
535
536 #[tokio::test]
540 async fn from_event_channel_propagates_errors() {
541 let (tx, rx) = mpsc::channel(8);
542 let mut stream = EventStream::from_event_channel(rx);
543
544 tx.send(Err(ClientError::Transport("frame decode failed".into())))
545 .await
546 .expect("send");
547 drop(tx);
548
549 let received = tokio::time::timeout(TEST_TIMEOUT, stream.next())
550 .await
551 .expect("timed out")
552 .expect("an error must be delivered, not swallowed");
553
554 assert!(
555 matches!(received, Err(ClientError::Transport(ref m)) if m == "frame decode failed"),
556 "the transport's own error must survive the bridge: {received:?}"
557 );
558 }
559
560 #[tokio::test]
562 async fn from_event_channel_ends_when_sender_drops() {
563 let (tx, rx) = mpsc::channel::<ClientResult<StreamResponse>>(8);
564 let mut stream = EventStream::from_event_channel(rx);
565 drop(tx);
566
567 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
568 .await
569 .expect("timed out");
570
571 assert!(result.is_none(), "a closed channel must end the stream");
572 }
573
574 #[tokio::test]
578 async fn from_event_channel_honours_terminal_events() {
579 let (tx, rx) = mpsc::channel(8);
580 let mut stream = EventStream::from_event_channel(rx);
581
582 tx.send(Ok(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
583 task_id: TaskId::new("task-1"),
584 context_id: a2a_protocol_types::ContextId::new("ctx-1"),
585 status: TaskStatus::new(TaskState::Completed),
586 metadata: None,
587 })))
588 .await
589 .expect("send");
590
591 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
592 .await
593 .expect("timed out")
594 .expect("the terminal event itself is delivered");
595 assert!(first.is_ok());
596
597 let next = tokio::time::timeout(TEST_TIMEOUT, stream.next())
600 .await
601 .expect("timed out");
602 assert!(
603 next.is_none(),
604 "a terminal event must end the stream even with the sender alive"
605 );
606 }
607
608 #[tokio::test]
609 async fn drop_aborts_background_task() {
610 let (tx, rx) = mpsc::channel::<BodyChunk>(8);
611 let handle = tokio::spawn(async move {
613 let _tx = tx;
615 tokio::time::sleep(Duration::from_secs(60 * 60)).await;
617 });
618 let abort_handle = handle.abort_handle();
619 let stream = EventStream::with_abort_handle(rx, abort_handle);
620 drop(stream);
622 let result = tokio::time::timeout(TEST_TIMEOUT, handle)
624 .await
625 .expect("timed out waiting for task abort");
626 assert!(result.is_err(), "task should have been aborted");
627 assert!(
628 result.unwrap_err().is_cancelled(),
629 "task should be cancelled"
630 );
631 }
632
633 #[test]
634 fn debug_output_contains_fields() {
635 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
636 let stream = EventStream::new(rx);
637 let debug = format!("{stream:?}");
638 assert!(debug.contains("EventStream"), "should contain struct name");
639 assert!(debug.contains("done"), "should contain 'done' field");
640 assert!(
641 debug.contains("pending_frames"),
642 "should contain 'pending_frames' field"
643 );
644 }
645
646 #[test]
647 fn is_terminal_returns_false_for_working() {
648 let event = make_status_event(TaskState::Working, false);
649 assert!(!is_terminal(&event), "Working state should not be terminal");
650 }
651
652 #[test]
653 fn is_terminal_returns_true_for_completed() {
654 let event = make_status_event(TaskState::Completed, true);
655 assert!(is_terminal(&event), "Completed state should be terminal");
656 }
657
658 #[tokio::test]
661 async fn stream_decodes_jsonrpc_error_as_protocol_error() {
662 use a2a_protocol_types::{JsonRpcErrorResponse, JsonRpcVersion};
663
664 let (tx, rx) = mpsc::channel(8);
665 let mut stream = EventStream::new(rx);
666
667 let error_resp = JsonRpcErrorResponse {
669 jsonrpc: JsonRpcVersion,
670 id: Some(serde_json::json!(1)),
671 error: a2a_protocol_types::JsonRpcError {
672 code: -32601,
673 message: "method not found".into(),
674 data: None,
675 },
676 };
677 let json = serde_json::to_string(&error_resp).unwrap();
678 let sse_data = format!("data: {json}\n\n");
679 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
680 drop(tx);
681
682 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
683 .await
684 .expect("timed out")
685 .unwrap();
686 assert!(result.is_err(), "JSON-RPC error should produce Err");
687 match result.unwrap_err() {
688 ClientError::Protocol(err) => {
689 assert!(
690 format!("{err}").contains("method not found"),
691 "error message should be preserved"
692 );
693 }
694 other => panic!("expected Protocol error, got {other:?}"),
695 }
696
697 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
699 .await
700 .expect("timed out");
701 assert!(end.is_none(), "stream should end after JSON-RPC error");
702 }
703
704 #[tokio::test]
707 async fn stream_invalid_json_returns_serialization_error() {
708 let (tx, rx) = mpsc::channel(8);
709 let mut stream = EventStream::new(rx);
710
711 let sse_data = "data: {not valid json}\n\n";
712 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
713 drop(tx);
714
715 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
716 .await
717 .expect("timed out")
718 .unwrap();
719 assert!(result.is_err(), "invalid JSON should produce Err");
720 assert!(
721 matches!(result.unwrap_err(), ClientError::Serialization(_)),
722 "should be a Serialization error"
723 );
724 }
725
726 #[tokio::test]
729 async fn stream_drains_parser_after_channel_close() {
730 let (tx, rx) = mpsc::channel(8);
731 let mut stream = EventStream::new(rx);
732
733 let event = make_status_event(TaskState::Working, false);
736 let sse_bytes = sse_frame(&event);
737 let (first_half, second_half) = sse_bytes.split_at(sse_bytes.len() / 2);
738
739 tx.send(Ok(Bytes::from(first_half.to_owned())))
740 .await
741 .unwrap();
742 tx.send(Ok(Bytes::from(second_half.to_owned())))
743 .await
744 .unwrap();
745 drop(tx);
746
747 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
748 .await
749 .expect("timed out")
750 .unwrap();
751 let event = result.unwrap();
752 assert!(
753 matches!(event, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working),
754 "should deliver Working event from drained parser"
755 );
756 }
757
758 #[tokio::test]
760 async fn status_code_returns_set_value() {
761 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
762 let stream = EventStream::new(rx);
763 assert_eq!(stream.status_code(), 200, "default status should be 200");
764 }
765
766 #[tokio::test]
768 async fn status_code_with_custom_value() {
769 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
770 let task = tokio::spawn(async { tokio::time::sleep(Duration::from_secs(60)).await });
771 let stream = EventStream::with_status(rx, task.abort_handle(), 201);
772 assert_eq!(stream.status_code(), 201);
773 }
774
775 #[tokio::test]
778 async fn first_event_timeout_fires_when_no_data_arrives() {
779 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
781 let mut stream = EventStream::new(rx).with_first_event_timeout(Duration::from_millis(50));
782 let result = tokio::time::timeout(Duration::from_secs(2), stream.next())
785 .await
786 .expect("first-event timeout must fire well within 2s");
787 assert!(
788 matches!(result, Some(Err(ClientError::Timeout(_)))),
789 "expected first-event timeout, got {result:?}"
790 );
791 let done = tokio::time::timeout(Duration::from_secs(2), stream.next())
793 .await
794 .expect("a completed stream must return promptly");
795 assert!(done.is_none());
796 }
797
798 #[tokio::test]
801 async fn first_event_timeout_lifted_after_first_chunk() {
802 let (tx, rx) = mpsc::channel(8);
803 let mut stream = EventStream::new(rx)
804 .with_jsonrpc_envelope(false)
805 .with_first_event_timeout(Duration::from_millis(50));
806 let event = make_status_event(TaskState::Working, false);
808 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
809 .await
810 .unwrap();
811 let first = stream.next().await;
812 assert!(
813 matches!(first, Some(Ok(_))),
814 "first event should parse, got {first:?}"
815 );
816 let pending = tokio::time::timeout(Duration::from_millis(120), stream.next()).await;
819 assert!(
820 pending.is_err(),
821 "stream must remain open (pending) after first chunk, got {pending:?}"
822 );
823 }
824
825 #[tokio::test]
828 async fn stream_transport_error_from_channel() {
829 let (tx, rx) = mpsc::channel(8);
830 let mut stream = EventStream::new(rx);
831
832 tx.send(Err(ClientError::HttpClient("connection reset".into())))
834 .await
835 .unwrap();
836
837 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
838 .await
839 .expect("timed out")
840 .unwrap();
841 match result {
842 Err(ClientError::HttpClient(msg)) => {
843 assert!(msg.contains("connection reset"));
844 }
845 other => panic!("expected HttpClient error, got {other:?}"),
846 }
847
848 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
850 .await
851 .expect("timed out");
852 assert!(end.is_none(), "stream should end after transport error");
853 }
854
855 #[tokio::test]
856 async fn non_terminal_event_does_not_end_stream() {
857 let (tx, rx) = mpsc::channel(8);
858 let mut stream = EventStream::new(rx);
859
860 let working = make_status_event(TaskState::Working, false);
862 let completed = make_status_event(TaskState::Completed, true);
863 tx.send(Ok(Bytes::from(sse_frame(&working)))).await.unwrap();
864 tx.send(Ok(Bytes::from(sse_frame(&completed))))
865 .await
866 .unwrap();
867
868 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
870 .await
871 .expect("timed out on first event")
872 .unwrap()
873 .unwrap();
874 assert!(
875 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
876 );
877
878 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
880 .await
881 .expect("timed out on second event")
882 .unwrap()
883 .unwrap();
884 assert!(
885 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
886 );
887
888 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
890 .await
891 .expect("timed out waiting for stream end");
892 assert!(end.is_none());
893 }
894
895 fn bare_sse_frame(event: &StreamResponse) -> String {
899 let json = serde_json::to_string(event).unwrap();
900 format!("data: {json}\n\n")
901 }
902
903 #[tokio::test]
904 async fn bare_stream_delivers_events() {
905 let (tx, rx) = mpsc::channel(8);
906 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
907
908 let event = make_status_event(TaskState::Working, false);
909 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
910 .await
911 .unwrap();
912 drop(tx);
913
914 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
915 .await
916 .expect("timed out")
917 .unwrap()
918 .unwrap();
919 assert!(
920 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
921 );
922 }
923
924 #[tokio::test]
925 async fn bare_stream_ends_on_terminal() {
926 let (tx, rx) = mpsc::channel(8);
927 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
928
929 let event = make_status_event(TaskState::Completed, true);
930 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
931 .await
932 .unwrap();
933
934 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
935 .await
936 .expect("timed out")
937 .unwrap()
938 .unwrap();
939 assert!(
940 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
941 );
942
943 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
944 .await
945 .expect("timed out");
946 assert!(end.is_none(), "bare stream should end after terminal event");
947 }
948
949 #[tokio::test]
950 async fn bare_stream_rejects_jsonrpc_envelope() {
951 let (tx, rx) = mpsc::channel(8);
952 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
953
954 let event = make_status_event(TaskState::Working, false);
956 let envelope_frame = sse_frame(&event); tx.send(Ok(Bytes::from(envelope_frame))).await.unwrap();
958 drop(tx);
959
960 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
961 .await
962 .expect("timed out")
963 .unwrap();
964 assert!(
965 result.is_err(),
966 "bare stream should reject JSON-RPC envelope as invalid"
967 );
968 }
969
970 #[tokio::test]
971 async fn envelope_stream_rejects_bare_response() {
972 let (tx, rx) = mpsc::channel(8);
973 let mut stream = EventStream::new(rx); let event = make_status_event(TaskState::Working, false);
977 let bare_frame = bare_sse_frame(&event);
978 tx.send(Ok(Bytes::from(bare_frame))).await.unwrap();
979 drop(tx);
980
981 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
982 .await
983 .expect("timed out")
984 .unwrap();
985 assert!(
986 result.is_err(),
987 "envelope stream should reject bare StreamResponse"
988 );
989 }
990
991 #[tokio::test]
992 async fn bare_stream_multiple_events() {
993 let (tx, rx) = mpsc::channel(8);
994 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
995
996 let working = make_status_event(TaskState::Working, false);
997 let completed = make_status_event(TaskState::Completed, true);
998 tx.send(Ok(Bytes::from(bare_sse_frame(&working))))
999 .await
1000 .unwrap();
1001 tx.send(Ok(Bytes::from(bare_sse_frame(&completed))))
1002 .await
1003 .unwrap();
1004
1005 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1006 .await
1007 .expect("timed out")
1008 .unwrap()
1009 .unwrap();
1010 assert!(
1011 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
1012 );
1013
1014 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1015 .await
1016 .expect("timed out")
1017 .unwrap()
1018 .unwrap();
1019 assert!(
1020 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
1021 );
1022
1023 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1024 .await
1025 .expect("timed out");
1026 assert!(end.is_none());
1027 }
1028}