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 held: Option<
109 Box<dyn std::any::Any + Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe>,
110 >,
111}
112
113impl EventStream {
114 #[must_use]
120 #[cfg(any(test, feature = "websocket"))]
121 pub(crate) fn new(rx: mpsc::Receiver<BodyChunk>) -> Self {
122 Self {
123 rx,
124 parser: SseParser::new(),
125 done: false,
126 abort_handle: None,
127 status_code: 200,
128 jsonrpc_envelope: true,
129 first_event_timeout: None,
130 first_chunk_received: false,
131 held: None,
132 }
133 }
134
135 #[must_use]
140 #[cfg(test)]
141 pub(crate) fn with_abort_handle(
142 rx: mpsc::Receiver<BodyChunk>,
143 abort_handle: AbortHandle,
144 ) -> Self {
145 Self {
146 rx,
147 parser: SseParser::new(),
148 done: false,
149 abort_handle: Some(abort_handle),
150 status_code: 200,
151 jsonrpc_envelope: true,
152 first_event_timeout: None,
153 first_chunk_received: false,
154 held: None,
155 }
156 }
157
158 #[must_use]
190 pub fn from_event_channel(mut rx: mpsc::Receiver<ClientResult<StreamResponse>>) -> Self {
191 let (tx, body_rx) = mpsc::channel::<BodyChunk>(EVENT_BRIDGE_CAPACITY);
192
193 let bridge = tokio::spawn(async move {
199 while let Some(event) = rx.recv().await {
200 let chunk = match event {
201 Ok(ref ev) => serde_json::to_string(ev).map_or_else(
202 |e| Err(ClientError::Serialization(e)),
203 |json| {
204 Ok(Bytes::from(format!(
205 "data: {{\"jsonrpc\":\"2.0\",\"id\":null,\"result\":{json}}}\n\n"
206 )))
207 },
208 ),
209 Err(e) => Err(e),
210 };
211 if tx.send(chunk).await.is_err() {
212 break;
213 }
214 }
215 });
216
217 Self::with_status(body_rx, bridge.abort_handle(), 200)
218 }
219
220 #[must_use]
223 pub(crate) fn with_status(
224 rx: mpsc::Receiver<BodyChunk>,
225 abort_handle: AbortHandle,
226 status_code: u16,
227 ) -> Self {
228 Self {
229 rx,
230 parser: SseParser::new(),
231 done: false,
232 abort_handle: Some(abort_handle),
233 status_code,
234 jsonrpc_envelope: true,
235 first_event_timeout: None,
236 first_chunk_received: false,
237 held: None,
238 }
239 }
240
241 #[must_use]
246 pub(crate) const fn with_jsonrpc_envelope(mut self, envelope: bool) -> Self {
247 self.jsonrpc_envelope = envelope;
248 self
249 }
250
251 #[must_use]
262 pub(crate) const fn with_first_event_timeout(mut self, timeout: std::time::Duration) -> Self {
263 self.first_event_timeout = Some(timeout);
264 self
265 }
266
267 #[must_use]
282 #[cfg(feature = "websocket")]
283 pub(crate) fn holding(
284 mut self,
285 resource: impl std::any::Any
286 + Send
287 + Sync
288 + std::panic::UnwindSafe
289 + std::panic::RefUnwindSafe
290 + 'static,
291 ) -> Self {
292 self.held = Some(Box::new(resource));
293 self
294 }
295
296 #[must_use]
301 pub const fn status_code(&self) -> u16 {
302 self.status_code
303 }
304
305 pub async fn next(&mut self) -> Option<ClientResult<StreamResponse>> {
312 loop {
313 if let Some(result) = self.parser.next_frame() {
315 match result {
316 Ok(frame) => return Some(self.decode_frame(&frame.data)),
317 Err(e) => {
318 return Some(Err(ClientError::Transport(e.to_string())));
319 }
320 }
321 }
322
323 if self.done {
324 return None;
325 }
326
327 let chunk = match self.first_event_timeout {
332 Some(timeout) if !self.first_chunk_received => {
333 let Ok(chunk) = tokio::time::timeout(timeout, self.rx.recv()).await else {
334 self.done = true;
335 return Some(Err(ClientError::Timeout(
336 "stream produced no data before the first-event timeout".into(),
337 )));
338 };
339 chunk
340 }
341 _ => self.rx.recv().await,
342 };
343 match chunk {
344 None => {
345 self.done = true;
347 if let Some(result) = self.parser.next_frame() {
349 match result {
350 Ok(frame) => return Some(self.decode_frame(&frame.data)),
351 Err(e) => {
352 return Some(Err(ClientError::Transport(e.to_string())));
353 }
354 }
355 }
356 return None;
357 }
358 Some(Err(e)) => {
359 self.done = true;
360 return Some(Err(e));
361 }
362 Some(Ok(bytes)) => {
363 self.first_chunk_received = true;
364 self.parser.feed(&bytes);
365 }
366 }
367 }
368 }
369
370 fn decode_frame(&mut self, data: &str) -> ClientResult<StreamResponse> {
373 if self.jsonrpc_envelope {
374 let envelope: JsonRpcResponse<StreamResponse> =
376 serde_json::from_str(data).map_err(ClientError::Serialization)?;
377
378 match envelope {
379 JsonRpcResponse::Success(ok) => {
380 if is_terminal(&ok.result) {
381 self.done = true;
382 }
383 Ok(ok.result)
384 }
385 JsonRpcResponse::Error(err) => {
386 self.done = true;
387 let a2a = crate::transport::map_jsonrpc_error(
388 err.error.code,
389 err.error.message,
390 err.error.data,
391 );
392 Err(ClientError::Protocol(a2a))
393 }
394 }
395 } else {
396 let event: StreamResponse =
399 serde_json::from_str(data).map_err(ClientError::Serialization)?;
400 if is_terminal(&event) {
401 self.done = true;
402 }
403 Ok(event)
404 }
405 }
406}
407
408impl Drop for EventStream {
409 fn drop(&mut self) {
410 if let Some(handle) = self.abort_handle.take() {
411 handle.abort();
412 }
413 drop(self.held.take());
420 }
421}
422
423#[allow(clippy::missing_fields_in_debug)]
424impl std::fmt::Debug for EventStream {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 f.debug_struct("EventStream")
428 .field("done", &self.done)
429 .field("pending_frames", &self.parser.pending_count())
430 .finish()
431 }
432}
433
434const fn is_terminal(event: &StreamResponse) -> bool {
436 matches!(
437 event,
438 StreamResponse::StatusUpdate(ev) if ev.status.state.is_terminal()
439 )
440}
441
442#[cfg(test)]
456const fn _event_stream_is_unwind_safe() {
457 const fn assert_unwind_safe<T: std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
458 assert_unwind_safe::<EventStream>();
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use a2a_protocol_types::{
465 JsonRpcSuccessResponse, JsonRpcVersion, TaskId, TaskState, TaskStatus,
466 TaskStatusUpdateEvent,
467 };
468 use std::time::Duration;
469
470 const TEST_TIMEOUT: Duration = Duration::from_secs(5);
473
474 fn make_status_event(state: TaskState, _is_final: bool) -> StreamResponse {
475 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
476 task_id: TaskId::new("t1"),
477 context_id: a2a_protocol_types::ContextId::new("c1"),
478 status: TaskStatus {
479 state,
480 message: None,
481 timestamp: None,
482 },
483 metadata: None,
484 })
485 }
486
487 fn sse_frame(event: &StreamResponse) -> String {
488 let resp = JsonRpcSuccessResponse {
489 jsonrpc: JsonRpcVersion,
490 id: Some(serde_json::json!(1)),
491 result: event.clone(),
492 };
493 let json = serde_json::to_string(&resp).unwrap();
494 format!("data: {json}\n\n")
495 }
496
497 #[tokio::test]
498 async fn stream_delivers_events() {
499 let (tx, rx) = mpsc::channel(8);
500 let mut stream = EventStream::new(rx);
501
502 let event = make_status_event(TaskState::Working, false);
503 let sse_bytes = sse_frame(&event);
504 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
505 drop(tx);
506
507 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
508 .await
509 .expect("timed out")
510 .unwrap()
511 .unwrap();
512 assert!(
513 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
514 );
515 }
516
517 #[tokio::test]
518 async fn stream_ends_on_final_event() {
519 let (tx, rx) = mpsc::channel(8);
520 let mut stream = EventStream::new(rx);
521
522 let event = make_status_event(TaskState::Completed, true);
523 let sse_bytes = sse_frame(&event);
524 tx.send(Ok(Bytes::from(sse_bytes))).await.unwrap();
525
526 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
528 .await
529 .expect("timed out waiting for final event")
530 .unwrap()
531 .unwrap();
532 assert!(
533 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
534 );
535
536 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
538 .await
539 .expect("timed out waiting for stream end");
540 assert!(end.is_none());
541 }
542
543 #[tokio::test]
544 async fn stream_propagates_body_error() {
545 let (tx, rx) = mpsc::channel(8);
546 let mut stream = EventStream::new(rx);
547
548 tx.send(Err(ClientError::Transport("network error".into())))
549 .await
550 .unwrap();
551
552 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
553 .await
554 .expect("timed out")
555 .unwrap();
556 assert!(result.is_err());
557 }
558
559 #[tokio::test]
560 async fn stream_ends_when_channel_closed() {
561 let (tx, rx) = mpsc::channel(8);
562 let mut stream = EventStream::new(rx);
563 drop(tx);
564
565 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
566 .await
567 .expect("timed out");
568 assert!(result.is_none());
569 }
570
571 #[tokio::test]
579 async fn from_event_channel_delivers_events() {
580 let (tx, rx) = mpsc::channel(8);
581 let mut stream = EventStream::from_event_channel(rx);
582
583 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
584 task_id: TaskId::new("task-1"),
585 context_id: a2a_protocol_types::ContextId::new("ctx-1"),
586 status: TaskStatus::new(TaskState::Working),
587 metadata: None,
588 });
589 tx.send(Ok(event)).await.expect("send");
590 drop(tx);
591
592 let received = tokio::time::timeout(TEST_TIMEOUT, stream.next())
593 .await
594 .expect("timed out")
595 .expect("a sent event must arrive")
596 .expect("and must not be an error");
597
598 match received {
599 StreamResponse::StatusUpdate(ev) => {
600 assert_eq!(ev.task_id, TaskId::new("task-1"));
601 assert_eq!(ev.status.state, TaskState::Working);
602 }
603 other => panic!("expected a status update, got {other:?}"),
604 }
605 }
606
607 #[tokio::test]
611 async fn from_event_channel_propagates_errors() {
612 let (tx, rx) = mpsc::channel(8);
613 let mut stream = EventStream::from_event_channel(rx);
614
615 tx.send(Err(ClientError::Transport("frame decode failed".into())))
616 .await
617 .expect("send");
618 drop(tx);
619
620 let received = tokio::time::timeout(TEST_TIMEOUT, stream.next())
621 .await
622 .expect("timed out")
623 .expect("an error must be delivered, not swallowed");
624
625 assert!(
626 matches!(received, Err(ClientError::Transport(ref m)) if m == "frame decode failed"),
627 "the transport's own error must survive the bridge: {received:?}"
628 );
629 }
630
631 #[tokio::test]
633 async fn from_event_channel_ends_when_sender_drops() {
634 let (tx, rx) = mpsc::channel::<ClientResult<StreamResponse>>(8);
635 let mut stream = EventStream::from_event_channel(rx);
636 drop(tx);
637
638 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
639 .await
640 .expect("timed out");
641
642 assert!(result.is_none(), "a closed channel must end the stream");
643 }
644
645 #[tokio::test]
649 async fn from_event_channel_honours_terminal_events() {
650 let (tx, rx) = mpsc::channel(8);
651 let mut stream = EventStream::from_event_channel(rx);
652
653 tx.send(Ok(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
654 task_id: TaskId::new("task-1"),
655 context_id: a2a_protocol_types::ContextId::new("ctx-1"),
656 status: TaskStatus::new(TaskState::Completed),
657 metadata: None,
658 })))
659 .await
660 .expect("send");
661
662 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
663 .await
664 .expect("timed out")
665 .expect("the terminal event itself is delivered");
666 assert!(first.is_ok());
667
668 let next = tokio::time::timeout(TEST_TIMEOUT, stream.next())
671 .await
672 .expect("timed out");
673 assert!(
674 next.is_none(),
675 "a terminal event must end the stream even with the sender alive"
676 );
677 }
678
679 #[tokio::test]
680 async fn drop_aborts_background_task() {
681 let (tx, rx) = mpsc::channel::<BodyChunk>(8);
682 let handle = tokio::spawn(async move {
684 let _tx = tx;
686 tokio::time::sleep(Duration::from_secs(60 * 60)).await;
688 });
689 let abort_handle = handle.abort_handle();
690 let stream = EventStream::with_abort_handle(rx, abort_handle);
691 drop(stream);
693 let result = tokio::time::timeout(TEST_TIMEOUT, handle)
695 .await
696 .expect("timed out waiting for task abort");
697 assert!(result.is_err(), "task should have been aborted");
698 assert!(
699 result.unwrap_err().is_cancelled(),
700 "task should be cancelled"
701 );
702 }
703
704 #[test]
705 fn debug_output_contains_fields() {
706 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
707 let stream = EventStream::new(rx);
708 let debug = format!("{stream:?}");
709 assert!(debug.contains("EventStream"), "should contain struct name");
710 assert!(debug.contains("done"), "should contain 'done' field");
711 assert!(
712 debug.contains("pending_frames"),
713 "should contain 'pending_frames' field"
714 );
715 }
716
717 #[test]
718 fn is_terminal_returns_false_for_working() {
719 let event = make_status_event(TaskState::Working, false);
720 assert!(!is_terminal(&event), "Working state should not be terminal");
721 }
722
723 #[test]
724 fn is_terminal_returns_true_for_completed() {
725 let event = make_status_event(TaskState::Completed, true);
726 assert!(is_terminal(&event), "Completed state should be terminal");
727 }
728
729 #[tokio::test]
732 async fn stream_decodes_jsonrpc_error_as_protocol_error() {
733 use a2a_protocol_types::{JsonRpcErrorResponse, JsonRpcVersion};
734
735 let (tx, rx) = mpsc::channel(8);
736 let mut stream = EventStream::new(rx);
737
738 let error_resp = JsonRpcErrorResponse {
740 jsonrpc: JsonRpcVersion,
741 id: Some(serde_json::json!(1)),
742 error: a2a_protocol_types::JsonRpcError {
743 code: -32601,
744 message: "method not found".into(),
745 data: None,
746 },
747 };
748 let json = serde_json::to_string(&error_resp).unwrap();
749 let sse_data = format!("data: {json}\n\n");
750 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
751 drop(tx);
752
753 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
754 .await
755 .expect("timed out")
756 .unwrap();
757 assert!(result.is_err(), "JSON-RPC error should produce Err");
758 match result.unwrap_err() {
759 ClientError::Protocol(err) => {
760 assert!(
761 format!("{err}").contains("method not found"),
762 "error message should be preserved"
763 );
764 }
765 other => panic!("expected Protocol error, got {other:?}"),
766 }
767
768 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
770 .await
771 .expect("timed out");
772 assert!(end.is_none(), "stream should end after JSON-RPC error");
773 }
774
775 #[tokio::test]
778 async fn stream_invalid_json_returns_serialization_error() {
779 let (tx, rx) = mpsc::channel(8);
780 let mut stream = EventStream::new(rx);
781
782 let sse_data = "data: {not valid json}\n\n";
783 tx.send(Ok(Bytes::from(sse_data))).await.unwrap();
784 drop(tx);
785
786 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
787 .await
788 .expect("timed out")
789 .unwrap();
790 assert!(result.is_err(), "invalid JSON should produce Err");
791 assert!(
792 matches!(result.unwrap_err(), ClientError::Serialization(_)),
793 "should be a Serialization error"
794 );
795 }
796
797 #[tokio::test]
800 async fn stream_drains_parser_after_channel_close() {
801 let (tx, rx) = mpsc::channel(8);
802 let mut stream = EventStream::new(rx);
803
804 let event = make_status_event(TaskState::Working, false);
807 let sse_bytes = sse_frame(&event);
808 let (first_half, second_half) = sse_bytes.split_at(sse_bytes.len() / 2);
809
810 tx.send(Ok(Bytes::from(first_half.to_owned())))
811 .await
812 .unwrap();
813 tx.send(Ok(Bytes::from(second_half.to_owned())))
814 .await
815 .unwrap();
816 drop(tx);
817
818 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
819 .await
820 .expect("timed out")
821 .unwrap();
822 let event = result.unwrap();
823 assert!(
824 matches!(event, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working),
825 "should deliver Working event from drained parser"
826 );
827 }
828
829 #[tokio::test]
831 async fn status_code_returns_set_value() {
832 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
833 let stream = EventStream::new(rx);
834 assert_eq!(stream.status_code(), 200, "default status should be 200");
835 }
836
837 #[tokio::test]
839 async fn status_code_with_custom_value() {
840 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
841 let task = tokio::spawn(async { tokio::time::sleep(Duration::from_secs(60)).await });
842 let stream = EventStream::with_status(rx, task.abort_handle(), 201);
843 assert_eq!(stream.status_code(), 201);
844 }
845
846 #[tokio::test]
849 async fn first_event_timeout_fires_when_no_data_arrives() {
850 let (_tx, rx) = mpsc::channel::<BodyChunk>(8);
852 let mut stream = EventStream::new(rx).with_first_event_timeout(Duration::from_millis(50));
853 let result = tokio::time::timeout(Duration::from_secs(2), stream.next())
856 .await
857 .expect("first-event timeout must fire well within 2s");
858 assert!(
859 matches!(result, Some(Err(ClientError::Timeout(_)))),
860 "expected first-event timeout, got {result:?}"
861 );
862 let done = tokio::time::timeout(Duration::from_secs(2), stream.next())
864 .await
865 .expect("a completed stream must return promptly");
866 assert!(done.is_none());
867 }
868
869 #[tokio::test]
872 async fn first_event_timeout_lifted_after_first_chunk() {
873 let (tx, rx) = mpsc::channel(8);
874 let mut stream = EventStream::new(rx)
875 .with_jsonrpc_envelope(false)
876 .with_first_event_timeout(Duration::from_millis(50));
877 let event = make_status_event(TaskState::Working, false);
879 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
880 .await
881 .unwrap();
882 let first = stream.next().await;
883 assert!(
884 matches!(first, Some(Ok(_))),
885 "first event should parse, got {first:?}"
886 );
887 let pending = tokio::time::timeout(Duration::from_millis(120), stream.next()).await;
890 assert!(
891 pending.is_err(),
892 "stream must remain open (pending) after first chunk, got {pending:?}"
893 );
894 }
895
896 #[tokio::test]
899 async fn stream_transport_error_from_channel() {
900 let (tx, rx) = mpsc::channel(8);
901 let mut stream = EventStream::new(rx);
902
903 tx.send(Err(ClientError::HttpClient("connection reset".into())))
905 .await
906 .unwrap();
907
908 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
909 .await
910 .expect("timed out")
911 .unwrap();
912 match result {
913 Err(ClientError::HttpClient(msg)) => {
914 assert!(msg.contains("connection reset"));
915 }
916 other => panic!("expected HttpClient error, got {other:?}"),
917 }
918
919 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
921 .await
922 .expect("timed out");
923 assert!(end.is_none(), "stream should end after transport error");
924 }
925
926 #[tokio::test]
927 async fn non_terminal_event_does_not_end_stream() {
928 let (tx, rx) = mpsc::channel(8);
929 let mut stream = EventStream::new(rx);
930
931 let working = make_status_event(TaskState::Working, false);
933 let completed = make_status_event(TaskState::Completed, true);
934 tx.send(Ok(Bytes::from(sse_frame(&working)))).await.unwrap();
935 tx.send(Ok(Bytes::from(sse_frame(&completed))))
936 .await
937 .unwrap();
938
939 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
941 .await
942 .expect("timed out on first event")
943 .unwrap()
944 .unwrap();
945 assert!(
946 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
947 );
948
949 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
951 .await
952 .expect("timed out on second event")
953 .unwrap()
954 .unwrap();
955 assert!(
956 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
957 );
958
959 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
961 .await
962 .expect("timed out waiting for stream end");
963 assert!(end.is_none());
964 }
965
966 fn bare_sse_frame(event: &StreamResponse) -> String {
970 let json = serde_json::to_string(event).unwrap();
971 format!("data: {json}\n\n")
972 }
973
974 #[tokio::test]
975 async fn bare_stream_delivers_events() {
976 let (tx, rx) = mpsc::channel(8);
977 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
978
979 let event = make_status_event(TaskState::Working, false);
980 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
981 .await
982 .unwrap();
983 drop(tx);
984
985 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
986 .await
987 .expect("timed out")
988 .unwrap()
989 .unwrap();
990 assert!(
991 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
992 );
993 }
994
995 #[tokio::test]
996 async fn bare_stream_ends_on_terminal() {
997 let (tx, rx) = mpsc::channel(8);
998 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
999
1000 let event = make_status_event(TaskState::Completed, true);
1001 tx.send(Ok(Bytes::from(bare_sse_frame(&event))))
1002 .await
1003 .unwrap();
1004
1005 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1006 .await
1007 .expect("timed out")
1008 .unwrap()
1009 .unwrap();
1010 assert!(
1011 matches!(result, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
1012 );
1013
1014 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1015 .await
1016 .expect("timed out");
1017 assert!(end.is_none(), "bare stream should end after terminal event");
1018 }
1019
1020 #[tokio::test]
1021 async fn bare_stream_rejects_jsonrpc_envelope() {
1022 let (tx, rx) = mpsc::channel(8);
1023 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
1024
1025 let event = make_status_event(TaskState::Working, false);
1027 let envelope_frame = sse_frame(&event); tx.send(Ok(Bytes::from(envelope_frame))).await.unwrap();
1029 drop(tx);
1030
1031 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1032 .await
1033 .expect("timed out")
1034 .unwrap();
1035 assert!(
1036 result.is_err(),
1037 "bare stream should reject JSON-RPC envelope as invalid"
1038 );
1039 }
1040
1041 #[tokio::test]
1042 async fn envelope_stream_rejects_bare_response() {
1043 let (tx, rx) = mpsc::channel(8);
1044 let mut stream = EventStream::new(rx); let event = make_status_event(TaskState::Working, false);
1048 let bare_frame = bare_sse_frame(&event);
1049 tx.send(Ok(Bytes::from(bare_frame))).await.unwrap();
1050 drop(tx);
1051
1052 let result = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1053 .await
1054 .expect("timed out")
1055 .unwrap();
1056 assert!(
1057 result.is_err(),
1058 "envelope stream should reject bare StreamResponse"
1059 );
1060 }
1061
1062 #[tokio::test]
1063 async fn bare_stream_multiple_events() {
1064 let (tx, rx) = mpsc::channel(8);
1065 let mut stream = EventStream::new(rx).with_jsonrpc_envelope(false);
1066
1067 let working = make_status_event(TaskState::Working, false);
1068 let completed = make_status_event(TaskState::Completed, true);
1069 tx.send(Ok(Bytes::from(bare_sse_frame(&working))))
1070 .await
1071 .unwrap();
1072 tx.send(Ok(Bytes::from(bare_sse_frame(&completed))))
1073 .await
1074 .unwrap();
1075
1076 let first = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1077 .await
1078 .expect("timed out")
1079 .unwrap()
1080 .unwrap();
1081 assert!(
1082 matches!(first, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Working)
1083 );
1084
1085 let second = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1086 .await
1087 .expect("timed out")
1088 .unwrap()
1089 .unwrap();
1090 assert!(
1091 matches!(second, StreamResponse::StatusUpdate(ref ev) if ev.status.state == TaskState::Completed)
1092 );
1093
1094 let end = tokio::time::timeout(TEST_TIMEOUT, stream.next())
1095 .await
1096 .expect("timed out");
1097 assert!(end.is_none());
1098 }
1099}