1use serde::{Deserialize, Serialize};
8
9use super::{
10 session::{FileDiff, Message, Part, Session},
11 shared::SessionError,
12};
13use crate::client::Opencode;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(tag = "type")]
24pub enum EventListResponse {
25 #[serde(rename = "installation.updated")]
28 InstallationUpdated {
29 properties: InstallationUpdatedProps,
31 },
32
33 #[serde(rename = "installation.update-available")]
35 InstallationUpdateAvailable {
36 properties: InstallationUpdateAvailableProps,
38 },
39
40 #[serde(rename = "project.updated")]
43 ProjectUpdated {
44 properties: ProjectUpdatedProps,
46 },
47
48 #[serde(rename = "server.instance.disposed")]
51 ServerInstanceDisposed {
52 properties: ServerInstanceDisposedProps,
54 },
55
56 #[serde(rename = "server.connected")]
58 ServerConnected {
59 properties: EmptyProps,
61 },
62
63 #[serde(rename = "global.disposed")]
66 GlobalDisposed {
67 properties: EmptyProps,
69 },
70
71 #[serde(rename = "lsp.client.diagnostics")]
74 LspClientDiagnostics {
75 properties: LspClientDiagnosticsProps,
77 },
78
79 #[serde(rename = "lsp.updated")]
81 LspUpdated {
82 properties: EmptyProps,
84 },
85
86 #[serde(rename = "file.edited")]
89 FileEdited {
90 properties: FileEditedProps,
92 },
93
94 #[serde(rename = "file.watcher.updated")]
96 FileWatcherUpdated {
97 properties: FileWatcherUpdatedProps,
99 },
100
101 #[serde(rename = "message.updated")]
104 MessageUpdated {
105 properties: MessageUpdatedProps,
107 },
108
109 #[serde(rename = "message.removed")]
111 MessageRemoved {
112 properties: MessageRemovedProps,
114 },
115
116 #[serde(rename = "message.part.updated")]
118 MessagePartUpdated {
119 properties: MessagePartUpdatedProps,
121 },
122
123 #[serde(rename = "message.part.delta")]
125 MessagePartDelta {
126 properties: MessagePartDeltaProps,
128 },
129
130 #[serde(rename = "message.part.removed")]
132 MessagePartRemoved {
133 properties: MessagePartRemovedProps,
135 },
136
137 #[serde(rename = "permission.asked")]
140 PermissionAsked {
141 properties: serde_json::Value,
143 },
144
145 #[serde(rename = "permission.replied")]
147 PermissionReplied {
148 properties: PermissionRepliedProps,
150 },
151
152 #[serde(rename = "session.created")]
155 SessionCreated {
156 properties: SessionCreatedProps,
158 },
159
160 #[serde(rename = "session.updated")]
162 SessionUpdated {
163 properties: SessionUpdatedProps,
165 },
166
167 #[serde(rename = "session.deleted")]
169 SessionDeleted {
170 properties: SessionDeletedProps,
172 },
173
174 #[serde(rename = "session.status")]
176 SessionStatus {
177 properties: SessionStatusProps,
179 },
180
181 #[serde(rename = "session.idle")]
183 SessionIdle {
184 properties: SessionIdleProps,
186 },
187
188 #[serde(rename = "session.diff")]
190 SessionDiff {
191 properties: SessionDiffProps,
193 },
194
195 #[serde(rename = "session.compacted")]
197 SessionCompacted {
198 properties: SessionCompactedProps,
200 },
201
202 #[serde(rename = "session.error")]
204 SessionError {
205 properties: SessionErrorProps,
207 },
208
209 #[serde(rename = "question.asked")]
212 QuestionAsked {
213 properties: serde_json::Value,
215 },
216
217 #[serde(rename = "question.replied")]
219 QuestionReplied {
220 properties: QuestionRepliedProps,
222 },
223
224 #[serde(rename = "question.rejected")]
226 QuestionRejected {
227 properties: QuestionRejectedProps,
229 },
230
231 #[serde(rename = "todo.updated")]
234 TodoUpdated {
235 properties: TodoUpdatedProps,
237 },
238
239 #[serde(rename = "tui.prompt.append")]
242 TuiPromptAppend {
243 properties: TuiPromptAppendProps,
245 },
246
247 #[serde(rename = "tui.command.execute")]
249 TuiCommandExecute {
250 properties: TuiCommandExecuteProps,
252 },
253
254 #[serde(rename = "tui.toast.show")]
256 TuiToastShow {
257 properties: TuiToastShowProps,
259 },
260
261 #[serde(rename = "tui.session.select")]
263 TuiSessionSelect {
264 properties: TuiSessionSelectProps,
266 },
267
268 #[serde(rename = "mcp.tools.changed")]
271 McpToolsChanged {
272 properties: McpToolsChangedProps,
274 },
275
276 #[serde(rename = "mcp.browser.open.failed")]
278 McpBrowserOpenFailed {
279 properties: McpBrowserOpenFailedProps,
281 },
282
283 #[serde(rename = "command.executed")]
286 CommandExecuted {
287 properties: CommandExecutedProps,
289 },
290
291 #[serde(rename = "vcs.branch.updated")]
294 VcsBranchUpdated {
295 properties: VcsBranchUpdatedProps,
297 },
298
299 #[serde(rename = "pty.created")]
302 PtyCreated {
303 properties: PtyCreatedProps,
305 },
306
307 #[serde(rename = "pty.updated")]
309 PtyUpdated {
310 properties: PtyUpdatedProps,
312 },
313
314 #[serde(rename = "pty.exited")]
316 PtyExited {
317 properties: PtyExitedProps,
319 },
320
321 #[serde(rename = "pty.deleted")]
323 PtyDeleted {
324 properties: PtyDeletedProps,
326 },
327
328 #[serde(rename = "worktree.ready")]
331 WorktreeReady {
332 properties: WorktreeReadyProps,
334 },
335
336 #[serde(rename = "worktree.failed")]
338 WorktreeFailed {
339 properties: WorktreeFailedProps,
341 },
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350pub struct EmptyProps {}
351
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
354pub struct InstallationUpdatedProps {
355 pub version: String,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361pub struct InstallationUpdateAvailableProps {
362 pub version: String,
364}
365
366#[allow(clippy::derive_partial_eq_without_eq)]
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371pub struct ProjectUpdatedProps {
372 pub properties: serde_json::Value,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378pub struct ServerInstanceDisposedProps {
379 pub directory: String,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385pub struct LspClientDiagnosticsProps {
386 pub path: String,
388 #[serde(rename = "serverID")]
390 pub server_id: String,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
395pub struct MessageUpdatedProps {
396 pub info: Message,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
402pub struct MessageRemovedProps {
403 #[serde(rename = "messageID")]
405 pub message_id: String,
406 #[serde(rename = "sessionID")]
408 pub session_id: String,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
413pub struct MessagePartUpdatedProps {
414 pub part: Part,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420pub struct MessagePartDeltaProps {
421 #[serde(rename = "sessionID")]
423 pub session_id: String,
424 #[serde(rename = "messageID")]
426 pub message_id: String,
427 #[serde(rename = "partID")]
429 pub part_id: String,
430 pub field: String,
432 pub delta: String,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
438pub struct MessagePartRemovedProps {
439 #[serde(rename = "sessionID")]
441 pub session_id: String,
442 #[serde(rename = "messageID")]
444 pub message_id: String,
445 #[serde(rename = "partID")]
447 pub part_id: String,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
452pub enum PermissionReply {
453 #[serde(rename = "once")]
455 Once,
456 #[serde(rename = "always")]
458 Always,
459 #[serde(rename = "reject")]
461 Reject,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct PermissionRepliedProps {
467 #[serde(rename = "sessionID")]
469 pub session_id: String,
470 #[serde(rename = "requestID")]
472 pub request_id: String,
473 pub reply: PermissionReply,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
479pub struct SessionCreatedProps {
480 pub info: Session,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
486pub struct SessionUpdatedProps {
487 pub info: Session,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
493pub struct SessionDeletedProps {
494 pub info: Session,
496}
497
498#[allow(clippy::derive_partial_eq_without_eq)]
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
501pub struct SessionStatusProps {
502 #[serde(rename = "sessionID")]
504 pub session_id: String,
505 pub status: serde_json::Value,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
511pub struct SessionIdleProps {
512 #[serde(rename = "sessionID")]
514 pub session_id: String,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
519pub struct SessionDiffProps {
520 #[serde(rename = "sessionID")]
522 pub session_id: String,
523 pub diff: Vec<FileDiff>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub struct SessionCompactedProps {
530 #[serde(rename = "sessionID")]
532 pub session_id: String,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
537pub struct SessionErrorProps {
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub error: Option<SessionError>,
541 #[serde(rename = "sessionID")]
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub session_id: Option<String>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
549pub struct QuestionRepliedProps {
550 #[serde(rename = "sessionID")]
552 pub session_id: String,
553 #[serde(rename = "requestID")]
555 pub request_id: String,
556 pub answers: Vec<Vec<String>>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
562pub struct QuestionRejectedProps {
563 #[serde(rename = "sessionID")]
565 pub session_id: String,
566 #[serde(rename = "requestID")]
568 pub request_id: String,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573pub struct Todo {
574 pub content: String,
576 pub status: String,
578 pub priority: String,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
584pub struct TodoUpdatedProps {
585 #[serde(rename = "sessionID")]
587 pub session_id: String,
588 pub todos: Vec<Todo>,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594pub struct FileEditedProps {
595 pub file: String,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601pub enum FileWatcherEvent {
602 #[serde(rename = "add")]
604 Add,
605 #[serde(rename = "change")]
607 Change,
608 #[serde(rename = "unlink")]
610 Unlink,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
615pub struct FileWatcherUpdatedProps {
616 pub event: FileWatcherEvent,
618 pub file: String,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
624pub struct TuiPromptAppendProps {
625 pub text: String,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631pub struct TuiCommandExecuteProps {
632 pub command: String,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
638pub enum ToastVariant {
639 #[serde(rename = "info")]
641 Info,
642 #[serde(rename = "success")]
644 Success,
645 #[serde(rename = "warning")]
647 Warning,
648 #[serde(rename = "error")]
650 Error,
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
655pub struct TuiToastShowProps {
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub title: Option<String>,
659 pub message: String,
661 pub variant: ToastVariant,
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub duration: Option<f64>,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
670pub struct TuiSessionSelectProps {
671 #[serde(rename = "sessionID")]
673 pub session_id: String,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
678pub struct McpToolsChangedProps {
679 pub server: String,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685pub struct McpBrowserOpenFailedProps {
686 #[serde(rename = "mcpName")]
688 pub mcp_name: String,
689 pub url: String,
691}
692
693#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
695pub struct CommandExecutedProps {
696 pub name: String,
698 #[serde(rename = "sessionID")]
700 pub session_id: String,
701 pub arguments: String,
703 #[serde(rename = "messageID")]
705 pub message_id: String,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
710pub struct VcsBranchUpdatedProps {
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub branch: Option<String>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
718pub enum PtyStatus {
719 #[serde(rename = "running")]
721 Running,
722 #[serde(rename = "exited")]
724 Exited,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
729pub struct Pty {
730 pub id: String,
732 pub title: String,
734 pub command: String,
736 pub args: Vec<String>,
738 pub cwd: String,
740 pub status: PtyStatus,
742 pub pid: f64,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
748pub struct PtyCreatedProps {
749 pub info: Pty,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
755pub struct PtyUpdatedProps {
756 pub info: Pty,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
762pub struct PtyExitedProps {
763 pub id: String,
765 #[serde(rename = "exitCode")]
767 pub exit_code: f64,
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
772pub struct PtyDeletedProps {
773 pub id: String,
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
779pub struct WorktreeReadyProps {
780 pub name: String,
782 pub branch: String,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
788pub struct WorktreeFailedProps {
789 pub message: String,
791}
792
793pub struct EventResource<'a> {
799 client: &'a Opencode,
800}
801
802impl<'a> EventResource<'a> {
803 pub(crate) const fn new(client: &'a Opencode) -> Self {
805 Self { client }
806 }
807
808 pub async fn list(
813 &self,
814 ) -> Result<crate::streaming::SseStream<EventListResponse>, crate::error::OpencodeError> {
815 self.client.get_stream("/event").await
816 }
817}
818
819#[cfg(test)]
824mod tests {
825 use super::*;
826 use crate::resources::session::{UserMessage, UserMessageModel, UserMessageTime};
827
828 #[test]
831 fn installation_updated_round_trip() {
832 let event = EventListResponse::InstallationUpdated {
833 properties: InstallationUpdatedProps { version: "1.2.3".into() },
834 };
835 let json_str = serde_json::to_string(&event).unwrap();
836 assert!(json_str.contains(r#""type":"installation.updated"#));
837 assert!(json_str.contains(r#""version":"1.2.3"#));
838 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
839 assert_eq!(event, back);
840 }
841
842 #[test]
845 fn message_updated_round_trip() {
846 let msg = Message::User(Box::new(UserMessage {
847 id: "msg_u001".into(),
848 session_id: "sess_001".into(),
849 time: UserMessageTime { created: 1_700_000_100.0 },
850 agent: "coder".into(),
851 model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
852 format: None,
853 summary: None,
854 system: None,
855 tools: None,
856 variant: None,
857 }));
858
859 let event = EventListResponse::MessageUpdated {
860 properties: MessageUpdatedProps { info: msg.clone() },
861 };
862 let json_str = serde_json::to_string(&event).unwrap();
863 assert!(json_str.contains(r#""type":"message.updated"#));
864 assert!(json_str.contains(r#""role":"user"#));
865 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
866 assert_eq!(event, back);
867 }
868
869 #[test]
872 fn session_error_round_trip() {
873 use crate::resources::shared::{SessionError as SE, UnknownErrorData};
874
875 let event = EventListResponse::SessionError {
876 properties: SessionErrorProps {
877 error: Some(SE::UnknownError {
878 data: UnknownErrorData { message: "something broke".into() },
879 }),
880 session_id: Some("sess_err_001".into()),
881 },
882 };
883 let json_str = serde_json::to_string(&event).unwrap();
884 assert!(json_str.contains(r#""type":"session.error"#));
885 assert!(json_str.contains(r#""name":"UnknownError"#));
886 assert!(json_str.contains("something broke"));
887 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
888 assert_eq!(event, back);
889 }
890
891 #[test]
892 fn session_error_empty_round_trip() {
893 let event = EventListResponse::SessionError {
894 properties: SessionErrorProps { error: None, session_id: None },
895 };
896 let json_str = serde_json::to_string(&event).unwrap();
897 assert!(!json_str.contains(r#""error""#));
899 assert!(!json_str.contains(r#""sessionID""#));
900 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
901 assert_eq!(event, back);
902 }
903
904 #[test]
907 fn file_watcher_updated_round_trip() {
908 let event = EventListResponse::FileWatcherUpdated {
909 properties: FileWatcherUpdatedProps {
910 event: FileWatcherEvent::Add,
911 file: "src/main.rs".into(),
912 },
913 };
914 let json_str = serde_json::to_string(&event).unwrap();
915 assert!(json_str.contains(r#""type":"file.watcher.updated"#));
916 assert!(json_str.contains(r#""event":"add"#));
917 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
918 assert_eq!(event, back);
919
920 let event2 = EventListResponse::FileWatcherUpdated {
922 properties: FileWatcherUpdatedProps {
923 event: FileWatcherEvent::Change,
924 file: "Cargo.toml".into(),
925 },
926 };
927 let json_str2 = serde_json::to_string(&event2).unwrap();
928 assert!(json_str2.contains(r#""event":"change"#));
929 let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
930 assert_eq!(event2, back2);
931
932 let event3 = EventListResponse::FileWatcherUpdated {
934 properties: FileWatcherUpdatedProps {
935 event: FileWatcherEvent::Unlink,
936 file: "old_file.rs".into(),
937 },
938 };
939 let json_str3 = serde_json::to_string(&event3).unwrap();
940 assert!(json_str3.contains(r#""event":"unlink"#));
941 let back3: EventListResponse = serde_json::from_str(&json_str3).unwrap();
942 assert_eq!(event3, back3);
943 }
944
945 #[test]
948 fn deserialize_permission_asked() {
949 let raw = r#"{
950 "type": "permission.asked",
951 "properties": {
952 "id": "perm_001",
953 "sessionID": "sess_001",
954 "title": "Run bash command"
955 }
956 }"#;
957 let event: EventListResponse = serde_json::from_str(raw).unwrap();
958 match &event {
959 EventListResponse::PermissionAsked { properties } => {
960 assert_eq!(properties["id"], "perm_001");
961 assert_eq!(properties["sessionID"], "sess_001");
962 }
963 other => panic!("expected PermissionAsked, got {other:?}"),
964 }
965 let json_str = serde_json::to_string(&event).unwrap();
967 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
968 assert_eq!(event, back);
969 }
970
971 #[test]
972 fn deserialize_permission_replied() {
973 let raw = r#"{
974 "type": "permission.replied",
975 "properties": {
976 "sessionID": "sess_001",
977 "requestID": "req_001",
978 "reply": "always"
979 }
980 }"#;
981 let event: EventListResponse = serde_json::from_str(raw).unwrap();
982 match &event {
983 EventListResponse::PermissionReplied { properties } => {
984 assert_eq!(properties.session_id, "sess_001");
985 assert_eq!(properties.request_id, "req_001");
986 assert_eq!(properties.reply, PermissionReply::Always);
987 }
988 other => panic!("expected PermissionReplied, got {other:?}"),
989 }
990 }
991
992 #[test]
995 fn lsp_client_diagnostics_round_trip() {
996 let event = EventListResponse::LspClientDiagnostics {
997 properties: LspClientDiagnosticsProps {
998 path: "src/main.rs".into(),
999 server_id: "rust-analyzer".into(),
1000 },
1001 };
1002 let json_str = serde_json::to_string(&event).unwrap();
1003 assert!(json_str.contains(r#""type":"lsp.client.diagnostics"#));
1004 assert!(json_str.contains(r#""serverID":"rust-analyzer"#));
1005 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1006 assert_eq!(event, back);
1007 }
1008
1009 #[test]
1010 fn message_removed_round_trip() {
1011 let event = EventListResponse::MessageRemoved {
1012 properties: MessageRemovedProps {
1013 message_id: "msg_del_001".into(),
1014 session_id: "sess_001".into(),
1015 },
1016 };
1017 let json_str = serde_json::to_string(&event).unwrap();
1018 assert!(json_str.contains(r#""type":"message.removed"#));
1019 assert!(json_str.contains("messageID"));
1020 assert!(json_str.contains("sessionID"));
1021 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1022 assert_eq!(event, back);
1023 }
1024
1025 #[test]
1026 fn message_part_updated_round_trip() {
1027 use crate::resources::session::{Part, TextPart};
1028
1029 let event = EventListResponse::MessagePartUpdated {
1030 properties: MessagePartUpdatedProps {
1031 part: Part::Text(TextPart {
1032 id: "p_upd_001".into(),
1033 message_id: "msg_001".into(),
1034 session_id: "sess_001".into(),
1035 text: "updated text".into(),
1036 synthetic: None,
1037 time: None,
1038 }),
1039 },
1040 };
1041 let json_str = serde_json::to_string(&event).unwrap();
1042 assert!(json_str.contains(r#""type":"message.part.updated"#));
1043 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1044 assert_eq!(event, back);
1045 }
1046
1047 #[test]
1048 fn message_part_removed_round_trip() {
1049 let event = EventListResponse::MessagePartRemoved {
1050 properties: MessagePartRemovedProps {
1051 session_id: "sess_001".into(),
1052 message_id: "msg_001".into(),
1053 part_id: "p_del_001".into(),
1054 },
1055 };
1056 let json_str = serde_json::to_string(&event).unwrap();
1057 assert!(json_str.contains(r#""type":"message.part.removed"#));
1058 assert!(json_str.contains("sessionID"));
1059 assert!(json_str.contains("messageID"));
1060 assert!(json_str.contains("partID"));
1061 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1062 assert_eq!(event, back);
1063 }
1064
1065 #[test]
1066 fn file_edited_round_trip() {
1067 let event = EventListResponse::FileEdited {
1068 properties: FileEditedProps { file: "src/lib.rs".into() },
1069 };
1070 let json_str = serde_json::to_string(&event).unwrap();
1071 assert!(json_str.contains(r#""type":"file.edited"#));
1072 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1073 assert_eq!(event, back);
1074 }
1075
1076 #[test]
1077 fn session_updated_round_trip() {
1078 let event = EventListResponse::SessionUpdated {
1079 properties: SessionUpdatedProps {
1080 info: Session {
1081 id: "sess_upd".into(),
1082 slug: String::new(),
1083 project_id: String::new(),
1084 directory: String::new(),
1085 time: crate::resources::session::SessionTime {
1086 created: 1_700_000_000.0,
1087 updated: 1_700_001_000.0,
1088 compacting: None,
1089 archived: None,
1090 },
1091 title: "Updated".into(),
1092 version: "1".into(),
1093 parent_id: None,
1094 revert: None,
1095 share: None,
1096 summary: None,
1097 permission: None,
1098 },
1099 },
1100 };
1101 let json_str = serde_json::to_string(&event).unwrap();
1102 assert!(json_str.contains(r#""type":"session.updated"#));
1103 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1104 assert_eq!(event, back);
1105 }
1106
1107 #[test]
1108 fn session_deleted_round_trip() {
1109 let event = EventListResponse::SessionDeleted {
1110 properties: SessionDeletedProps {
1111 info: Session {
1112 id: "sess_del".into(),
1113 slug: String::new(),
1114 project_id: String::new(),
1115 directory: String::new(),
1116 time: crate::resources::session::SessionTime {
1117 created: 1_700_000_000.0,
1118 updated: 1_700_000_000.0,
1119 compacting: None,
1120 archived: None,
1121 },
1122 title: "Deleted".into(),
1123 version: "1".into(),
1124 parent_id: None,
1125 revert: None,
1126 share: None,
1127 summary: None,
1128 permission: None,
1129 },
1130 },
1131 };
1132 let json_str = serde_json::to_string(&event).unwrap();
1133 assert!(json_str.contains(r#""type":"session.deleted"#));
1134 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1135 assert_eq!(event, back);
1136 }
1137
1138 #[test]
1139 fn session_idle_round_trip() {
1140 let event = EventListResponse::SessionIdle {
1141 properties: SessionIdleProps { session_id: "sess_idle_001".into() },
1142 };
1143 let json_str = serde_json::to_string(&event).unwrap();
1144 assert!(json_str.contains(r#""type":"session.idle"#));
1145 assert!(json_str.contains("sessionID"));
1146 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1147 assert_eq!(event, back);
1148 }
1149
1150 #[test]
1151 fn session_error_both_fields_null() {
1152 let raw = r#"{
1154 "type": "session.error",
1155 "properties": { "error": null, "sessionID": null }
1156 }"#;
1157 let event: EventListResponse = serde_json::from_str(raw).unwrap();
1158 match &event {
1159 EventListResponse::SessionError { properties } => {
1160 assert_eq!(properties.error, None);
1161 assert_eq!(properties.session_id, None);
1162 }
1163 other => panic!("expected SessionError, got {other:?}"),
1164 }
1165 }
1166
1167 #[test]
1170 fn installation_update_available_round_trip() {
1171 let event = EventListResponse::InstallationUpdateAvailable {
1172 properties: InstallationUpdateAvailableProps { version: "2.0.0".into() },
1173 };
1174 let json_str = serde_json::to_string(&event).unwrap();
1175 assert!(json_str.contains(r#""type":"installation.update-available"#));
1176 assert!(json_str.contains(r#""version":"2.0.0"#));
1177 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1178 assert_eq!(event, back);
1179 }
1180
1181 #[test]
1182 fn message_part_delta_round_trip() {
1183 let event = EventListResponse::MessagePartDelta {
1184 properties: MessagePartDeltaProps {
1185 session_id: "sess_001".into(),
1186 message_id: "msg_001".into(),
1187 part_id: "part_001".into(),
1188 field: "text".into(),
1189 delta: "hello ".into(),
1190 },
1191 };
1192 let json_str = serde_json::to_string(&event).unwrap();
1193 assert!(json_str.contains(r#""type":"message.part.delta"#));
1194 assert!(json_str.contains(r#""sessionID":"sess_001"#));
1195 assert!(json_str.contains(r#""delta":"hello "#));
1196 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1197 assert_eq!(event, back);
1198 }
1199
1200 #[test]
1201 fn server_connected_round_trip() {
1202 let event = EventListResponse::ServerConnected { properties: EmptyProps {} };
1203 let json_str = serde_json::to_string(&event).unwrap();
1204 assert!(json_str.contains(r#""type":"server.connected"#));
1205 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1206 assert_eq!(event, back);
1207 }
1208
1209 #[test]
1210 fn tui_toast_show_round_trip() {
1211 let event = EventListResponse::TuiToastShow {
1212 properties: TuiToastShowProps {
1213 title: Some("Heads up".into()),
1214 message: "Build succeeded".into(),
1215 variant: ToastVariant::Success,
1216 duration: Some(5.0),
1217 },
1218 };
1219 let json_str = serde_json::to_string(&event).unwrap();
1220 assert!(json_str.contains(r#""type":"tui.toast.show"#));
1221 assert!(json_str.contains(r#""variant":"success"#));
1222 assert!(json_str.contains(r#""title":"Heads up"#));
1223 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1224 assert_eq!(event, back);
1225
1226 let event2 = EventListResponse::TuiToastShow {
1228 properties: TuiToastShowProps {
1229 title: None,
1230 message: "Error occurred".into(),
1231 variant: ToastVariant::Error,
1232 duration: None,
1233 },
1234 };
1235 let json_str2 = serde_json::to_string(&event2).unwrap();
1236 assert!(!json_str2.contains(r#""title""#));
1237 assert!(!json_str2.contains(r#""duration""#));
1238 let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
1239 assert_eq!(event2, back2);
1240 }
1241
1242 #[test]
1243 fn todo_updated_round_trip() {
1244 let event = EventListResponse::TodoUpdated {
1245 properties: TodoUpdatedProps {
1246 session_id: "sess_001".into(),
1247 todos: vec![
1248 Todo {
1249 content: "Fix bug".into(),
1250 status: "pending".into(),
1251 priority: "high".into(),
1252 },
1253 Todo {
1254 content: "Write docs".into(),
1255 status: "done".into(),
1256 priority: "low".into(),
1257 },
1258 ],
1259 },
1260 };
1261 let json_str = serde_json::to_string(&event).unwrap();
1262 assert!(json_str.contains(r#""type":"todo.updated"#));
1263 assert!(json_str.contains("Fix bug"));
1264 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1265 assert_eq!(event, back);
1266 }
1267
1268 #[test]
1269 fn worktree_ready_round_trip() {
1270 let event = EventListResponse::WorktreeReady {
1271 properties: WorktreeReadyProps {
1272 name: "feature-branch".into(),
1273 branch: "feat/new-feature".into(),
1274 },
1275 };
1276 let json_str = serde_json::to_string(&event).unwrap();
1277 assert!(json_str.contains(r#""type":"worktree.ready"#));
1278 assert!(json_str.contains(r#""name":"feature-branch"#));
1279 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1280 assert_eq!(event, back);
1281 }
1282
1283 #[test]
1284 fn question_replied_round_trip() {
1285 let event = EventListResponse::QuestionReplied {
1286 properties: QuestionRepliedProps {
1287 session_id: "sess_001".into(),
1288 request_id: "req_001".into(),
1289 answers: vec![vec!["yes".into(), "no".into()]],
1290 },
1291 };
1292 let json_str = serde_json::to_string(&event).unwrap();
1293 assert!(json_str.contains(r#""type":"question.replied"#));
1294 assert!(json_str.contains(r#""sessionID":"sess_001"#));
1295 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1296 assert_eq!(event, back);
1297 }
1298
1299 #[test]
1300 fn mcp_tools_changed_round_trip() {
1301 let event = EventListResponse::McpToolsChanged {
1302 properties: McpToolsChangedProps { server: "my-mcp-server".into() },
1303 };
1304 let json_str = serde_json::to_string(&event).unwrap();
1305 assert!(json_str.contains(r#""type":"mcp.tools.changed"#));
1306 assert!(json_str.contains(r#""server":"my-mcp-server"#));
1307 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1308 assert_eq!(event, back);
1309 }
1310
1311 #[test]
1312 fn pty_created_round_trip() {
1313 let event = EventListResponse::PtyCreated {
1314 properties: PtyCreatedProps {
1315 info: Pty {
1316 id: "pty_001".into(),
1317 title: "shell".into(),
1318 command: "/bin/zsh".into(),
1319 args: vec!["-l".into()],
1320 cwd: "/home/user".into(),
1321 status: PtyStatus::Running,
1322 pid: 12345.0,
1323 },
1324 },
1325 };
1326 let json_str = serde_json::to_string(&event).unwrap();
1327 assert!(json_str.contains(r#""type":"pty.created"#));
1328 assert!(json_str.contains(r#""status":"running"#));
1329 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1330 assert_eq!(event, back);
1331 }
1332
1333 #[test]
1334 fn vcs_branch_updated_round_trip() {
1335 let event = EventListResponse::VcsBranchUpdated {
1336 properties: VcsBranchUpdatedProps { branch: Some("main".into()) },
1337 };
1338 let json_str = serde_json::to_string(&event).unwrap();
1339 assert!(json_str.contains(r#""type":"vcs.branch.updated"#));
1340 assert!(json_str.contains(r#""branch":"main"#));
1341 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1342 assert_eq!(event, back);
1343
1344 let event2 = EventListResponse::VcsBranchUpdated {
1346 properties: VcsBranchUpdatedProps { branch: None },
1347 };
1348 let json_str2 = serde_json::to_string(&event2).unwrap();
1349 assert!(!json_str2.contains(r#""branch""#));
1350 let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
1351 assert_eq!(event2, back2);
1352 }
1353
1354 #[test]
1355 fn command_executed_round_trip() {
1356 let event = EventListResponse::CommandExecuted {
1357 properties: CommandExecutedProps {
1358 name: "test-cmd".into(),
1359 session_id: "sess_001".into(),
1360 arguments: "{}".into(),
1361 message_id: "msg_001".into(),
1362 },
1363 };
1364 let json_str = serde_json::to_string(&event).unwrap();
1365 assert!(json_str.contains(r#""type":"command.executed"#));
1366 assert!(json_str.contains(r#""sessionID":"sess_001"#));
1367 assert!(json_str.contains(r#""messageID":"msg_001"#));
1368 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1369 assert_eq!(event, back);
1370 }
1371
1372 #[test]
1373 fn deserialize_project_updated_from_raw() {
1374 let raw = r#"{
1375 "type": "project.updated",
1376 "properties": {
1377 "properties": { "name": "my-project", "path": "/tmp/proj" }
1378 }
1379 }"#;
1380 let event: EventListResponse = serde_json::from_str(raw).unwrap();
1381 match &event {
1382 EventListResponse::ProjectUpdated { properties } => {
1383 assert_eq!(properties.properties["name"], "my-project");
1384 }
1385 other => panic!("expected ProjectUpdated, got {other:?}"),
1386 }
1387 }
1388
1389 #[test]
1390 fn deserialize_session_status_from_raw() {
1391 let raw = r#"{
1392 "type": "session.status",
1393 "properties": {
1394 "sessionID": "sess_001",
1395 "status": { "type": "running", "tool": "bash" }
1396 }
1397 }"#;
1398 let event: EventListResponse = serde_json::from_str(raw).unwrap();
1399 match &event {
1400 EventListResponse::SessionStatus { properties } => {
1401 assert_eq!(properties.session_id, "sess_001");
1402 assert_eq!(properties.status["type"], "running");
1403 }
1404 other => panic!("expected SessionStatus, got {other:?}"),
1405 }
1406 }
1407}