1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::shared::SessionError;
8use crate::{
9 client::{Opencode, RequestOptions},
10 error::OpencodeError,
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct Session {
20 pub id: String,
22 pub time: SessionTime,
24 pub title: String,
26 pub version: String,
28 #[serde(rename = "parentID")]
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub parent_id: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub revert: Option<SessionRevert>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub share: Option<SessionShare>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct SessionTime {
43 pub created: f64,
45 pub updated: f64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct SessionRevert {
52 #[serde(rename = "messageID")]
54 pub message_id: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub diff: Option<String>,
58 #[serde(rename = "partID")]
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub part_id: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub snapshot: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct SessionShare {
70 pub url: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80pub struct UserMessage {
81 pub id: String,
83 #[serde(rename = "sessionID")]
85 pub session_id: String,
86 pub time: UserMessageTime,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct UserMessageTime {
93 pub created: f64,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub struct AssistantMessage {
100 pub id: String,
102 pub cost: f64,
104 pub mode: String,
106 #[serde(rename = "modelID")]
108 pub model_id: String,
109 pub path: AssistantMessagePath,
111 #[serde(rename = "providerID")]
113 pub provider_id: String,
114 #[serde(rename = "sessionID")]
116 pub session_id: String,
117 pub system: Vec<String>,
119 pub time: AssistantMessageTime,
121 pub tokens: AssistantMessageTokens,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub error: Option<SessionError>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub summary: Option<bool>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133pub struct AssistantMessagePath {
134 pub cwd: String,
136 pub root: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
142pub struct AssistantMessageTime {
143 pub created: f64,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub completed: Option<f64>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
152pub struct AssistantMessageTokens {
153 pub cache: TokenCache,
155 pub input: u64,
157 pub output: u64,
159 pub reasoning: u64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165pub struct TokenCache {
166 pub read: u64,
168 pub write: u64,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174#[serde(tag = "role")]
175pub enum Message {
176 #[serde(rename = "user")]
178 User(UserMessage),
179 #[serde(rename = "assistant")]
181 Assistant(Box<AssistantMessage>),
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct TextPart {
191 pub id: String,
193 #[serde(rename = "messageID")]
195 pub message_id: String,
196 #[serde(rename = "sessionID")]
198 pub session_id: String,
199 pub text: String,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub synthetic: Option<bool>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub time: Option<TextPartTime>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211pub struct TextPartTime {
212 pub start: f64,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub end: Option<f64>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
221pub struct FilePart {
222 pub id: String,
224 #[serde(rename = "messageID")]
226 pub message_id: String,
227 pub mime: String,
229 #[serde(rename = "sessionID")]
231 pub session_id: String,
232 pub url: String,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub filename: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub source: Option<FilePartSource>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub struct ToolPart {
245 pub id: String,
247 #[serde(rename = "callID")]
249 pub call_id: String,
250 #[serde(rename = "messageID")]
252 pub message_id: String,
253 #[serde(rename = "sessionID")]
255 pub session_id: String,
256 pub state: ToolState,
258 pub tool: String,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
264pub struct StepStartPart {
265 pub id: String,
267 #[serde(rename = "messageID")]
269 pub message_id: String,
270 #[serde(rename = "sessionID")]
272 pub session_id: String,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277pub struct StepFinishPart {
278 pub id: String,
280 pub cost: f64,
282 #[serde(rename = "messageID")]
284 pub message_id: String,
285 #[serde(rename = "sessionID")]
287 pub session_id: String,
288 pub tokens: StepFinishTokens,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
294pub struct StepFinishTokens {
295 pub cache: TokenCache,
297 pub input: u64,
299 pub output: u64,
301 pub reasoning: u64,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
307pub struct SnapshotPart {
308 pub id: String,
310 #[serde(rename = "messageID")]
312 pub message_id: String,
313 #[serde(rename = "sessionID")]
315 pub session_id: String,
316 pub snapshot: String,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
322pub struct PatchPart {
323 pub id: String,
325 pub files: Vec<String>,
327 pub hash: String,
329 #[serde(rename = "messageID")]
331 pub message_id: String,
332 #[serde(rename = "sessionID")]
334 pub session_id: String,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
339#[serde(tag = "type")]
340pub enum Part {
341 #[serde(rename = "text")]
343 Text(TextPart),
344 #[serde(rename = "file")]
346 File(FilePart),
347 #[serde(rename = "tool")]
349 Tool(ToolPart),
350 #[serde(rename = "step-start")]
352 StepStart(StepStartPart),
353 #[serde(rename = "step-finish")]
355 StepFinish(StepFinishPart),
356 #[serde(rename = "snapshot")]
358 Snapshot(SnapshotPart),
359 #[serde(rename = "patch")]
361 Patch(PatchPart),
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370pub struct ToolStatePending {}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
374pub struct ToolStateRunning {
375 pub time: ToolStateRunningTime,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub input: Option<serde_json::Value>,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub metadata: Option<HashMap<String, serde_json::Value>>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub title: Option<String>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390pub struct ToolStateRunningTime {
391 pub start: f64,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
397pub struct ToolStateCompleted {
398 pub input: HashMap<String, serde_json::Value>,
400 pub metadata: HashMap<String, serde_json::Value>,
402 pub output: String,
404 pub time: ToolStateCompletedTime,
406 pub title: String,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412pub struct ToolStateCompletedTime {
413 pub end: f64,
415 pub start: f64,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
421pub struct ToolStateError {
422 pub error: String,
424 pub input: HashMap<String, serde_json::Value>,
426 pub time: ToolStateErrorTime,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432pub struct ToolStateErrorTime {
433 pub end: f64,
435 pub start: f64,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
441#[serde(tag = "status")]
442pub enum ToolState {
443 #[serde(rename = "pending")]
445 Pending(ToolStatePending),
446 #[serde(rename = "running")]
448 Running(ToolStateRunning),
449 #[serde(rename = "completed")]
451 Completed(ToolStateCompleted),
452 #[serde(rename = "error")]
454 Error(ToolStateError),
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
463pub struct FilePartSourceText {
464 pub end: u64,
466 pub start: u64,
468 pub value: String,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
474pub struct FileSource {
475 pub path: String,
477 pub text: FilePartSourceText,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
483pub struct SymbolSource {
484 pub kind: u64,
486 pub name: String,
488 pub path: String,
490 pub range: SymbolSourceRange,
492 pub text: FilePartSourceText,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
498pub struct SymbolSourceRange {
499 pub end: SymbolSourcePosition,
501 pub start: SymbolSourcePosition,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
507pub struct SymbolSourcePosition {
508 pub character: u64,
510 pub line: u64,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
516#[serde(tag = "type")]
517pub enum FilePartSource {
518 #[serde(rename = "file")]
520 File(FileSource),
521 #[serde(rename = "symbol")]
523 Symbol(SymbolSource),
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
532pub struct TextPartInput {
533 pub text: String,
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub id: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub synthetic: Option<bool>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub time: Option<TextPartInputTime>,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
548pub struct TextPartInputTime {
549 pub start: f64,
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub end: Option<f64>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
558pub struct FilePartInput {
559 pub mime: String,
561 pub url: String,
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub id: Option<String>,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub filename: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub source: Option<FilePartSource>,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576#[serde(tag = "type")]
577pub enum PartInput {
578 #[serde(rename = "text")]
580 Text(TextPartInput),
581 #[serde(rename = "file")]
583 File(FilePartInput),
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
592pub struct SessionMessagesResponseItem {
593 pub info: Message,
595 pub parts: Vec<Part>,
597}
598
599pub type SessionMessagesResponse = Vec<SessionMessagesResponseItem>;
601
602pub type SessionListResponse = Vec<Session>;
604
605pub type SessionDeleteResponse = bool;
607
608pub type SessionAbortResponse = bool;
610
611pub type SessionInitResponse = bool;
613
614pub type SessionSummarizeResponse = bool;
616
617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
623pub struct SessionChatParams {
624 #[serde(rename = "modelID")]
626 pub model_id: String,
627 pub parts: Vec<PartInput>,
629 #[serde(rename = "providerID")]
631 pub provider_id: String,
632 #[serde(rename = "messageID")]
634 #[serde(skip_serializing_if = "Option::is_none")]
635 pub message_id: Option<String>,
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub mode: Option<String>,
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub system: Option<String>,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 pub tools: Option<HashMap<String, bool>>,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
649pub struct SessionInitParams {
650 #[serde(rename = "messageID")]
652 pub message_id: String,
653 #[serde(rename = "modelID")]
655 pub model_id: String,
656 #[serde(rename = "providerID")]
658 pub provider_id: String,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
663pub struct SessionRevertParams {
664 #[serde(rename = "messageID")]
666 pub message_id: String,
667 #[serde(rename = "partID")]
669 #[serde(skip_serializing_if = "Option::is_none")]
670 pub part_id: Option<String>,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
675pub struct SessionSummarizeParams {
676 #[serde(rename = "modelID")]
678 pub model_id: String,
679 #[serde(rename = "providerID")]
681 pub provider_id: String,
682}
683
684pub struct SessionResource<'a> {
690 client: &'a Opencode,
691}
692
693impl<'a> SessionResource<'a> {
694 pub(crate) const fn new(client: &'a Opencode) -> Self {
696 Self { client }
697 }
698
699 pub async fn create(&self, options: Option<&RequestOptions>) -> Result<Session, OpencodeError> {
701 self.client.post::<Session, ()>("/session", None, options).await
702 }
703
704 pub async fn list(
706 &self,
707 options: Option<&RequestOptions>,
708 ) -> Result<SessionListResponse, OpencodeError> {
709 self.client.get("/session", options).await
710 }
711
712 pub async fn delete(
714 &self,
715 id: &str,
716 options: Option<&RequestOptions>,
717 ) -> Result<SessionDeleteResponse, OpencodeError> {
718 self.client.delete::<bool, ()>(&format!("/session/{id}"), None, options).await
719 }
720
721 pub async fn abort(
723 &self,
724 id: &str,
725 options: Option<&RequestOptions>,
726 ) -> Result<SessionAbortResponse, OpencodeError> {
727 self.client.post::<bool, ()>(&format!("/session/{id}/abort"), None, options).await
728 }
729
730 pub async fn chat(
732 &self,
733 id: &str,
734 params: &SessionChatParams,
735 options: Option<&RequestOptions>,
736 ) -> Result<AssistantMessage, OpencodeError> {
737 self.client.post(&format!("/session/{id}/message"), Some(params), options).await
738 }
739
740 pub async fn init(
742 &self,
743 id: &str,
744 params: &SessionInitParams,
745 options: Option<&RequestOptions>,
746 ) -> Result<SessionInitResponse, OpencodeError> {
747 self.client.post(&format!("/session/{id}/init"), Some(params), options).await
748 }
749
750 pub async fn messages(
752 &self,
753 id: &str,
754 options: Option<&RequestOptions>,
755 ) -> Result<SessionMessagesResponse, OpencodeError> {
756 self.client.get(&format!("/session/{id}/message"), options).await
757 }
758
759 pub async fn revert(
761 &self,
762 id: &str,
763 params: &SessionRevertParams,
764 options: Option<&RequestOptions>,
765 ) -> Result<Session, OpencodeError> {
766 self.client.post(&format!("/session/{id}/revert"), Some(params), options).await
767 }
768
769 pub async fn share(
771 &self,
772 id: &str,
773 options: Option<&RequestOptions>,
774 ) -> Result<Session, OpencodeError> {
775 self.client.post::<Session, ()>(&format!("/session/{id}/share"), None, options).await
776 }
777
778 pub async fn summarize(
780 &self,
781 id: &str,
782 params: &SessionSummarizeParams,
783 options: Option<&RequestOptions>,
784 ) -> Result<SessionSummarizeResponse, OpencodeError> {
785 self.client.post(&format!("/session/{id}/summarize"), Some(params), options).await
786 }
787
788 pub async fn unrevert(
790 &self,
791 id: &str,
792 options: Option<&RequestOptions>,
793 ) -> Result<Session, OpencodeError> {
794 self.client.post::<Session, ()>(&format!("/session/{id}/unrevert"), None, options).await
795 }
796
797 pub async fn unshare(
799 &self,
800 id: &str,
801 options: Option<&RequestOptions>,
802 ) -> Result<Session, OpencodeError> {
803 self.client.delete::<Session, ()>(&format!("/session/{id}/share"), None, options).await
804 }
805}
806
807#[cfg(test)]
812mod tests {
813 use serde_json::json;
814
815 use super::*;
816
817 #[test]
820 fn session_full_round_trip() {
821 let session = Session {
822 id: "sess_001".into(),
823 time: SessionTime { created: 1_700_000_000.0, updated: 1_700_001_000.0 },
824 title: "My Session".into(),
825 version: "1".into(),
826 parent_id: Some("sess_000".into()),
827 revert: Some(SessionRevert {
828 message_id: "msg_001".into(),
829 diff: Some("--- a/file\n+++ b/file".into()),
830 part_id: Some("part_001".into()),
831 snapshot: Some("snapshot_data".into()),
832 }),
833 share: Some(SessionShare { url: "https://example.com/share/abc".into() }),
834 };
835 let json_str = serde_json::to_string(&session).unwrap();
836 assert!(json_str.contains("parentID"));
837 assert!(json_str.contains("messageID"));
838 assert!(json_str.contains("partID"));
839 let back: Session = serde_json::from_str(&json_str).unwrap();
840 assert_eq!(session, back);
841 }
842
843 #[test]
844 fn session_minimal_round_trip() {
845 let session = Session {
846 id: "sess_002".into(),
847 time: SessionTime { created: 1_700_000_000.0, updated: 1_700_000_000.0 },
848 title: "Empty".into(),
849 version: "1".into(),
850 parent_id: None,
851 revert: None,
852 share: None,
853 };
854 let json_str = serde_json::to_string(&session).unwrap();
855 assert!(!json_str.contains("parentID"));
856 assert!(!json_str.contains("revert"));
857 assert!(!json_str.contains("share"));
858 let back: Session = serde_json::from_str(&json_str).unwrap();
859 assert_eq!(session, back);
860 }
861
862 #[test]
865 fn user_message_round_trip() {
866 let msg = UserMessage {
867 id: "msg_u001".into(),
868 session_id: "sess_001".into(),
869 time: UserMessageTime { created: 1_700_000_100.0 },
870 };
871 let json_str = serde_json::to_string(&msg).unwrap();
872 assert!(json_str.contains("sessionID"));
873 let back: UserMessage = serde_json::from_str(&json_str).unwrap();
874 assert_eq!(msg, back);
875 }
876
877 #[test]
878 fn assistant_message_round_trip() {
879 let msg = AssistantMessage {
880 id: "msg_a001".into(),
881 cost: 0.0032,
882 mode: "code".into(),
883 model_id: "gpt-4o".into(),
884 path: AssistantMessagePath {
885 cwd: "/home/user/project".into(),
886 root: "/home/user/project".into(),
887 },
888 provider_id: "openai".into(),
889 session_id: "sess_001".into(),
890 system: vec!["You are a helpful assistant.".into()],
891 time: AssistantMessageTime {
892 created: 1_700_000_200.0,
893 completed: Some(1_700_000_210.0),
894 },
895 tokens: AssistantMessageTokens {
896 cache: TokenCache { read: 100, write: 50 },
897 input: 500,
898 output: 200,
899 reasoning: 0,
900 },
901 error: None,
902 summary: None,
903 };
904 let json_str = serde_json::to_string(&msg).unwrap();
905 assert!(json_str.contains("modelID"));
906 assert!(json_str.contains("providerID"));
907 assert!(json_str.contains("sessionID"));
908 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
909 assert_eq!(msg, back);
910 }
911
912 #[test]
913 fn assistant_message_with_error() {
914 let msg = AssistantMessage {
915 id: "msg_a002".into(),
916 cost: 0.0,
917 mode: "code".into(),
918 model_id: "gpt-4o".into(),
919 path: AssistantMessagePath { cwd: "/tmp".into(), root: "/tmp".into() },
920 provider_id: "openai".into(),
921 session_id: "sess_001".into(),
922 system: vec![],
923 time: AssistantMessageTime { created: 1_700_000_300.0, completed: None },
924 tokens: AssistantMessageTokens {
925 cache: TokenCache { read: 0, write: 0 },
926 input: 0,
927 output: 0,
928 reasoning: 0,
929 },
930 error: Some(SessionError::ProviderAuthError {
931 data: super::super::shared::ProviderAuthErrorData {
932 message: "invalid key".into(),
933 provider_id: "openai".into(),
934 },
935 }),
936 summary: Some(true),
937 };
938 let json_str = serde_json::to_string(&msg).unwrap();
939 assert!(json_str.contains("ProviderAuthError"));
940 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
941 assert_eq!(msg, back);
942 }
943
944 #[test]
947 fn message_enum_user_variant() {
948 let msg = Message::User(UserMessage {
949 id: "msg_u002".into(),
950 session_id: "sess_001".into(),
951 time: UserMessageTime { created: 1_700_000_100.0 },
952 });
953 let json_str = serde_json::to_string(&msg).unwrap();
954 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
955 assert_eq!(v["role"], "user");
956 let back: Message = serde_json::from_str(&json_str).unwrap();
957 assert_eq!(msg, back);
958 }
959
960 #[test]
961 fn message_enum_assistant_variant() {
962 let msg = Message::Assistant(Box::new(AssistantMessage {
963 id: "msg_a003".into(),
964 cost: 0.001,
965 mode: "default".into(),
966 model_id: "claude-3-opus".into(),
967 path: AssistantMessagePath { cwd: "/home".into(), root: "/home".into() },
968 provider_id: "anthropic".into(),
969 session_id: "sess_002".into(),
970 system: vec![],
971 time: AssistantMessageTime {
972 created: 1_700_000_500.0,
973 completed: Some(1_700_000_510.0),
974 },
975 tokens: AssistantMessageTokens {
976 cache: TokenCache { read: 10, write: 5 },
977 input: 100,
978 output: 50,
979 reasoning: 20,
980 },
981 error: None,
982 summary: None,
983 }));
984 let json_str = serde_json::to_string(&msg).unwrap();
985 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
986 assert_eq!(v["role"], "assistant");
987 let back: Message = serde_json::from_str(&json_str).unwrap();
988 assert_eq!(msg, back);
989 }
990
991 #[test]
994 fn part_text_round_trip() {
995 let part = Part::Text(TextPart {
996 id: "p_001".into(),
997 message_id: "msg_a001".into(),
998 session_id: "sess_001".into(),
999 text: "Hello, world!".into(),
1000 synthetic: None,
1001 time: Some(TextPartTime { start: 1_700_000_200.0, end: Some(1_700_000_201.0) }),
1002 });
1003 let json_str = serde_json::to_string(&part).unwrap();
1004 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1005 assert_eq!(v["type"], "text");
1006 let back: Part = serde_json::from_str(&json_str).unwrap();
1007 assert_eq!(part, back);
1008 }
1009
1010 #[test]
1011 fn part_tool_round_trip() {
1012 let part = Part::Tool(ToolPart {
1013 id: "p_002".into(),
1014 call_id: "call_001".into(),
1015 message_id: "msg_a001".into(),
1016 session_id: "sess_001".into(),
1017 state: ToolState::Completed(ToolStateCompleted {
1018 input: HashMap::from([("cmd".into(), json!("ls"))]),
1019 metadata: HashMap::new(),
1020 output: "file1.rs\nfile2.rs".into(),
1021 time: ToolStateCompletedTime { end: 1_700_000_205.0, start: 1_700_000_202.0 },
1022 title: "bash".into(),
1023 }),
1024 tool: "bash".into(),
1025 });
1026 let json_str = serde_json::to_string(&part).unwrap();
1027 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1028 assert_eq!(v["type"], "tool");
1029 assert_eq!(v["state"]["status"], "completed");
1030 let back: Part = serde_json::from_str(&json_str).unwrap();
1031 assert_eq!(part, back);
1032 }
1033
1034 #[test]
1035 fn part_step_start_round_trip() {
1036 let part = Part::StepStart(StepStartPart {
1037 id: "p_003".into(),
1038 message_id: "msg_a001".into(),
1039 session_id: "sess_001".into(),
1040 });
1041 let json_str = serde_json::to_string(&part).unwrap();
1042 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1043 assert_eq!(v["type"], "step-start");
1044 let back: Part = serde_json::from_str(&json_str).unwrap();
1045 assert_eq!(part, back);
1046 }
1047
1048 #[test]
1049 fn part_step_finish_round_trip() {
1050 let part = Part::StepFinish(StepFinishPart {
1051 id: "p_004".into(),
1052 cost: 0.001,
1053 message_id: "msg_a001".into(),
1054 session_id: "sess_001".into(),
1055 tokens: StepFinishTokens {
1056 cache: TokenCache { read: 10, write: 5 },
1057 input: 100,
1058 output: 50,
1059 reasoning: 0,
1060 },
1061 });
1062 let json_str = serde_json::to_string(&part).unwrap();
1063 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1064 assert_eq!(v["type"], "step-finish");
1065 let back: Part = serde_json::from_str(&json_str).unwrap();
1066 assert_eq!(part, back);
1067 }
1068
1069 #[test]
1070 fn part_patch_round_trip() {
1071 let part = Part::Patch(PatchPart {
1072 id: "p_005".into(),
1073 files: vec!["src/main.rs".into(), "Cargo.toml".into()],
1074 hash: "abc123".into(),
1075 message_id: "msg_a001".into(),
1076 session_id: "sess_001".into(),
1077 });
1078 let json_str = serde_json::to_string(&part).unwrap();
1079 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1080 assert_eq!(v["type"], "patch");
1081 let back: Part = serde_json::from_str(&json_str).unwrap();
1082 assert_eq!(part, back);
1083 }
1084
1085 #[test]
1086 fn part_snapshot_round_trip() {
1087 let part = Part::Snapshot(SnapshotPart {
1088 id: "p_006".into(),
1089 message_id: "msg_a001".into(),
1090 session_id: "sess_001".into(),
1091 snapshot: "{\"state\":\"data\"}".into(),
1092 });
1093 let json_str = serde_json::to_string(&part).unwrap();
1094 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1095 assert_eq!(v["type"], "snapshot");
1096 let back: Part = serde_json::from_str(&json_str).unwrap();
1097 assert_eq!(part, back);
1098 }
1099
1100 #[test]
1101 fn part_file_round_trip() {
1102 let part = Part::File(FilePart {
1103 id: "p_007".into(),
1104 message_id: "msg_a001".into(),
1105 mime: "image/png".into(),
1106 session_id: "sess_001".into(),
1107 url: "https://example.com/img.png".into(),
1108 filename: Some("screenshot.png".into()),
1109 source: None,
1110 });
1111 let json_str = serde_json::to_string(&part).unwrap();
1112 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1113 assert_eq!(v["type"], "file");
1114 let back: Part = serde_json::from_str(&json_str).unwrap();
1115 assert_eq!(part, back);
1116 }
1117
1118 #[test]
1121 fn tool_state_pending() {
1122 let state = ToolState::Pending(ToolStatePending {});
1123 let json_str = serde_json::to_string(&state).unwrap();
1124 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1125 assert_eq!(v["status"], "pending");
1126 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1127 assert_eq!(state, back);
1128 }
1129
1130 #[test]
1131 fn tool_state_running() {
1132 let state = ToolState::Running(ToolStateRunning {
1133 time: ToolStateRunningTime { start: 1_700_000_200.0 },
1134 input: Some(json!({"command": "echo hello"})),
1135 metadata: Some(HashMap::from([("key".into(), json!("value"))])),
1136 title: Some("Running bash".into()),
1137 });
1138 let json_str = serde_json::to_string(&state).unwrap();
1139 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1140 assert_eq!(v["status"], "running");
1141 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1142 assert_eq!(state, back);
1143 }
1144
1145 #[test]
1146 fn tool_state_completed() {
1147 let state = ToolState::Completed(ToolStateCompleted {
1148 input: HashMap::from([("cmd".into(), json!("ls -la"))]),
1149 metadata: HashMap::from([("exit_code".into(), json!(0))]),
1150 output: "total 42\ndrwxr-xr-x ...".into(),
1151 time: ToolStateCompletedTime { end: 1_700_000_210.0, start: 1_700_000_200.0 },
1152 title: "bash".into(),
1153 });
1154 let json_str = serde_json::to_string(&state).unwrap();
1155 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1156 assert_eq!(v["status"], "completed");
1157 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1158 assert_eq!(state, back);
1159 }
1160
1161 #[test]
1162 fn tool_state_error() {
1163 let state = ToolState::Error(ToolStateError {
1164 error: "command not found".into(),
1165 input: HashMap::from([("cmd".into(), json!("nonexistent"))]),
1166 time: ToolStateErrorTime { end: 1_700_000_201.0, start: 1_700_000_200.0 },
1167 });
1168 let json_str = serde_json::to_string(&state).unwrap();
1169 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1170 assert_eq!(v["status"], "error");
1171 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1172 assert_eq!(state, back);
1173 }
1174
1175 #[test]
1178 fn file_part_source_file_variant() {
1179 let src = FilePartSource::File(FileSource {
1180 path: "/home/user/main.rs".into(),
1181 text: FilePartSourceText { end: 100, start: 0, value: "fn main() {}".into() },
1182 });
1183 let json_str = serde_json::to_string(&src).unwrap();
1184 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1185 assert_eq!(v["type"], "file");
1186 let back: FilePartSource = serde_json::from_str(&json_str).unwrap();
1187 assert_eq!(src, back);
1188 }
1189
1190 #[test]
1191 fn file_part_source_symbol_variant() {
1192 let src = FilePartSource::Symbol(SymbolSource {
1193 kind: 12,
1194 name: "main".into(),
1195 path: "/home/user/main.rs".into(),
1196 range: SymbolSourceRange {
1197 end: SymbolSourcePosition { character: 1, line: 2 },
1198 start: SymbolSourcePosition { character: 0, line: 0 },
1199 },
1200 text: FilePartSourceText {
1201 end: 50,
1202 start: 0,
1203 value: "fn main() {\n println!(\"hello\");\n}".into(),
1204 },
1205 });
1206 let json_str = serde_json::to_string(&src).unwrap();
1207 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1208 assert_eq!(v["type"], "symbol");
1209 let back: FilePartSource = serde_json::from_str(&json_str).unwrap();
1210 assert_eq!(src, back);
1211 }
1212
1213 #[test]
1216 fn session_chat_params_full_round_trip() {
1217 let params = SessionChatParams {
1218 model_id: "gpt-4o".into(),
1219 parts: vec![
1220 PartInput::Text(TextPartInput {
1221 text: "Hello!".into(),
1222 id: Some("input_001".into()),
1223 synthetic: None,
1224 time: Some(TextPartInputTime { start: 1_700_000_000.0, end: None }),
1225 }),
1226 PartInput::File(FilePartInput {
1227 mime: "text/plain".into(),
1228 url: "file:///tmp/test.txt".into(),
1229 id: None,
1230 filename: Some("test.txt".into()),
1231 source: None,
1232 }),
1233 ],
1234 provider_id: "openai".into(),
1235 message_id: Some("msg_001".into()),
1236 mode: Some("code".into()),
1237 system: Some("Be concise.".into()),
1238 tools: Some(HashMap::from([("bash".into(), true)])),
1239 };
1240 let json_str = serde_json::to_string(¶ms).unwrap();
1241 assert!(json_str.contains("modelID"));
1242 assert!(json_str.contains("providerID"));
1243 assert!(json_str.contains("messageID"));
1244 let back: SessionChatParams = serde_json::from_str(&json_str).unwrap();
1245 assert_eq!(params, back);
1246 }
1247
1248 #[test]
1249 fn session_chat_params_minimal() {
1250 let params = SessionChatParams {
1251 model_id: "gpt-4o".into(),
1252 parts: vec![PartInput::Text(TextPartInput {
1253 text: "Hi".into(),
1254 id: None,
1255 synthetic: None,
1256 time: None,
1257 })],
1258 provider_id: "openai".into(),
1259 message_id: None,
1260 mode: None,
1261 system: None,
1262 tools: None,
1263 };
1264 let json_str = serde_json::to_string(¶ms).unwrap();
1265 assert!(!json_str.contains("messageID"));
1266 assert!(!json_str.contains("\"mode\""));
1267 assert!(!json_str.contains("system"));
1268 assert!(!json_str.contains("tools"));
1269 let back: SessionChatParams = serde_json::from_str(&json_str).unwrap();
1270 assert_eq!(params, back);
1271 }
1272
1273 #[test]
1276 fn part_input_text_round_trip() {
1277 let input = PartInput::Text(TextPartInput {
1278 text: "Hello".into(),
1279 id: None,
1280 synthetic: Some(true),
1281 time: None,
1282 });
1283 let json_str = serde_json::to_string(&input).unwrap();
1284 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1285 assert_eq!(v["type"], "text");
1286 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1287 assert_eq!(input, back);
1288 }
1289
1290 #[test]
1291 fn part_input_file_round_trip() {
1292 let input = PartInput::File(FilePartInput {
1293 mime: "image/png".into(),
1294 url: "https://example.com/img.png".into(),
1295 id: Some("fi_001".into()),
1296 filename: Some("photo.png".into()),
1297 source: Some(FilePartSource::File(FileSource {
1298 path: "/tmp/photo.png".into(),
1299 text: FilePartSourceText { end: 0, start: 0, value: String::new() },
1300 })),
1301 });
1302 let json_str = serde_json::to_string(&input).unwrap();
1303 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1304 assert_eq!(v["type"], "file");
1305 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1306 assert_eq!(input, back);
1307 }
1308
1309 #[test]
1312 fn session_messages_response_item_round_trip() {
1313 let item = SessionMessagesResponseItem {
1314 info: Message::User(UserMessage {
1315 id: "msg_u010".into(),
1316 session_id: "sess_001".into(),
1317 time: UserMessageTime { created: 1_700_000_000.0 },
1318 }),
1319 parts: vec![Part::Text(TextPart {
1320 id: "p_010".into(),
1321 message_id: "msg_u010".into(),
1322 session_id: "sess_001".into(),
1323 text: "What is Rust?".into(),
1324 synthetic: None,
1325 time: None,
1326 })],
1327 };
1328 let json_str = serde_json::to_string(&item).unwrap();
1329 let back: SessionMessagesResponseItem = serde_json::from_str(&json_str).unwrap();
1330 assert_eq!(item, back);
1331 }
1332
1333 #[test]
1336 fn session_init_params_round_trip() {
1337 let params = SessionInitParams {
1338 message_id: "msg_001".into(),
1339 model_id: "gpt-4o".into(),
1340 provider_id: "openai".into(),
1341 };
1342 let json_str = serde_json::to_string(¶ms).unwrap();
1343 assert!(json_str.contains("messageID"));
1344 assert!(json_str.contains("modelID"));
1345 assert!(json_str.contains("providerID"));
1346 let back: SessionInitParams = serde_json::from_str(&json_str).unwrap();
1347 assert_eq!(params, back);
1348 }
1349
1350 #[test]
1351 fn session_revert_params_round_trip() {
1352 let params =
1353 SessionRevertParams { message_id: "msg_001".into(), part_id: Some("part_001".into()) };
1354 let json_str = serde_json::to_string(¶ms).unwrap();
1355 assert!(json_str.contains("messageID"));
1356 assert!(json_str.contains("partID"));
1357 let back: SessionRevertParams = serde_json::from_str(&json_str).unwrap();
1358 assert_eq!(params, back);
1359 }
1360
1361 #[test]
1362 fn session_summarize_params_round_trip() {
1363 let params =
1364 SessionSummarizeParams { model_id: "gpt-4o".into(), provider_id: "openai".into() };
1365 let json_str = serde_json::to_string(¶ms).unwrap();
1366 assert!(json_str.contains("modelID"));
1367 assert!(json_str.contains("providerID"));
1368 let back: SessionSummarizeParams = serde_json::from_str(&json_str).unwrap();
1369 assert_eq!(params, back);
1370 }
1371
1372 #[test]
1375 fn deserialize_message_from_js_json() {
1376 let js_json = json!({
1377 "role": "user",
1378 "id": "msg_from_js",
1379 "sessionID": "sess_js",
1380 "time": { "created": 1700000000.0 }
1381 });
1382 let msg: Message = serde_json::from_value(js_json).unwrap();
1383 match msg {
1384 Message::User(u) => {
1385 assert_eq!(u.id, "msg_from_js");
1386 assert_eq!(u.session_id, "sess_js");
1387 }
1388 _ => panic!("expected User variant"),
1389 }
1390 }
1391
1392 #[test]
1393 fn deserialize_part_from_js_json() {
1394 let js_json = json!({
1395 "type": "step-start",
1396 "id": "p_js_001",
1397 "messageID": "msg_js_001",
1398 "sessionID": "sess_js"
1399 });
1400 let part: Part = serde_json::from_value(js_json).unwrap();
1401 match part {
1402 Part::StepStart(s) => {
1403 assert_eq!(s.id, "p_js_001");
1404 assert_eq!(s.message_id, "msg_js_001");
1405 }
1406 _ => panic!("expected StepStart variant"),
1407 }
1408 }
1409
1410 #[test]
1411 fn deserialize_tool_state_from_js_json() {
1412 let js_json = json!({
1413 "status": "error",
1414 "error": "timeout",
1415 "input": { "cmd": "sleep 999" },
1416 "time": { "start": 1700000000.0, "end": 1700000030.0 }
1417 });
1418 let state: ToolState = serde_json::from_value(js_json).unwrap();
1419 match state {
1420 ToolState::Error(e) => {
1421 assert_eq!(e.error, "timeout");
1422 }
1423 _ => panic!("expected Error variant"),
1424 }
1425 }
1426
1427 #[test]
1430 fn tool_state_running_minimal() {
1431 let state = ToolState::Running(ToolStateRunning {
1432 time: ToolStateRunningTime { start: 1_700_000_000.0 },
1433 input: None,
1434 metadata: None,
1435 title: None,
1436 });
1437 let json_str = serde_json::to_string(&state).unwrap();
1438 assert!(!json_str.contains("input"));
1439 assert!(!json_str.contains("metadata"));
1440 assert!(!json_str.contains("title"));
1441 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1442 assert_eq!(v["status"], "running");
1443 let back: ToolState = serde_json::from_str(&json_str).unwrap();
1444 assert_eq!(state, back);
1445 }
1446
1447 #[test]
1448 fn text_part_no_synthetic_no_time() {
1449 let part = TextPart {
1450 id: "tp_001".into(),
1451 message_id: "msg_001".into(),
1452 session_id: "sess_001".into(),
1453 text: "bare text".into(),
1454 synthetic: None,
1455 time: None,
1456 };
1457 let json_str = serde_json::to_string(&part).unwrap();
1458 assert!(!json_str.contains("synthetic"));
1459 assert!(!json_str.contains("time"));
1460 let back: TextPart = serde_json::from_str(&json_str).unwrap();
1461 assert_eq!(part, back);
1462 }
1463
1464 #[test]
1465 fn file_part_no_filename_no_source() {
1466 let part = FilePart {
1467 id: "fp_001".into(),
1468 message_id: "msg_001".into(),
1469 mime: "application/octet-stream".into(),
1470 session_id: "sess_001".into(),
1471 url: "https://example.com/data.bin".into(),
1472 filename: None,
1473 source: None,
1474 };
1475 let json_str = serde_json::to_string(&part).unwrap();
1476 assert!(!json_str.contains("filename"));
1477 assert!(!json_str.contains("source"));
1478 let back: FilePart = serde_json::from_str(&json_str).unwrap();
1479 assert_eq!(part, back);
1480 }
1481
1482 #[test]
1483 fn part_file_minimal_round_trip() {
1484 let part = Part::File(FilePart {
1485 id: "fp_002".into(),
1486 message_id: "msg_001".into(),
1487 mime: "text/plain".into(),
1488 session_id: "sess_001".into(),
1489 url: "file:///tmp/a.txt".into(),
1490 filename: None,
1491 source: None,
1492 });
1493 let json_str = serde_json::to_string(&part).unwrap();
1494 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1495 assert_eq!(v["type"], "file");
1496 assert!(v.get("filename").is_none());
1497 assert!(v.get("source").is_none());
1498 let back: Part = serde_json::from_str(&json_str).unwrap();
1499 assert_eq!(part, back);
1500 }
1501
1502 #[test]
1503 fn assistant_message_no_error_no_summary() {
1504 let msg = AssistantMessage {
1505 id: "msg_edge".into(),
1506 cost: 0.0,
1507 mode: "plan".into(),
1508 model_id: "o1".into(),
1509 path: AssistantMessagePath { cwd: "/app".into(), root: "/app".into() },
1510 provider_id: "openai".into(),
1511 session_id: "sess_edge".into(),
1512 system: vec![],
1513 time: AssistantMessageTime { created: 1_700_000_000.0, completed: None },
1514 tokens: AssistantMessageTokens {
1515 cache: TokenCache { read: 0, write: 0 },
1516 input: 10,
1517 output: 5,
1518 reasoning: 0,
1519 },
1520 error: None,
1521 summary: None,
1522 };
1523 let json_str = serde_json::to_string(&msg).unwrap();
1524 assert!(!json_str.contains("error"));
1525 assert!(!json_str.contains("summary"));
1526 let back: AssistantMessage = serde_json::from_str(&json_str).unwrap();
1527 assert_eq!(msg, back);
1528 }
1529
1530 #[test]
1531 fn part_input_text_minimal() {
1532 let input = PartInput::Text(TextPartInput {
1533 text: "hi".into(),
1534 id: None,
1535 synthetic: None,
1536 time: None,
1537 });
1538 let json_str = serde_json::to_string(&input).unwrap();
1539 assert!(!json_str.contains("\"id\""));
1540 assert!(!json_str.contains("synthetic"));
1541 assert!(!json_str.contains("time"));
1542 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1543 assert_eq!(input, back);
1544 }
1545
1546 #[test]
1547 fn part_input_file_minimal() {
1548 let input = PartInput::File(FilePartInput {
1549 mime: "text/csv".into(),
1550 url: "file:///data.csv".into(),
1551 id: None,
1552 filename: None,
1553 source: None,
1554 });
1555 let json_str = serde_json::to_string(&input).unwrap();
1556 assert!(!json_str.contains("\"id\""));
1557 assert!(!json_str.contains("filename"));
1558 assert!(!json_str.contains("source"));
1559 let back: PartInput = serde_json::from_str(&json_str).unwrap();
1560 assert_eq!(input, back);
1561 }
1562
1563 #[test]
1564 fn session_revert_minimal() {
1565 let revert = SessionRevert {
1566 message_id: "msg_r001".into(),
1567 diff: None,
1568 part_id: None,
1569 snapshot: None,
1570 };
1571 let json_str = serde_json::to_string(&revert).unwrap();
1572 assert!(!json_str.contains("diff"));
1573 assert!(!json_str.contains("partID"));
1574 assert!(!json_str.contains("snapshot"));
1575 let back: SessionRevert = serde_json::from_str(&json_str).unwrap();
1576 assert_eq!(revert, back);
1577 }
1578
1579 #[test]
1580 fn text_part_time_no_end() {
1581 let t = TextPartTime { start: 1_700_000_000.0, end: None };
1582 let json_str = serde_json::to_string(&t).unwrap();
1583 assert!(!json_str.contains("end"));
1584 let back: TextPartTime = serde_json::from_str(&json_str).unwrap();
1585 assert_eq!(t, back);
1586 }
1587
1588 #[test]
1589 fn assistant_message_time_no_completed() {
1590 let t = AssistantMessageTime { created: 1_700_000_000.0, completed: None };
1591 let json_str = serde_json::to_string(&t).unwrap();
1592 assert!(!json_str.contains("completed"));
1593 let back: AssistantMessageTime = serde_json::from_str(&json_str).unwrap();
1594 assert_eq!(t, back);
1595 }
1596
1597 #[test]
1598 fn session_revert_params_no_part_id() {
1599 let params = SessionRevertParams { message_id: "msg_001".into(), part_id: None };
1600 let json_str = serde_json::to_string(¶ms).unwrap();
1601 assert!(!json_str.contains("partID"));
1602 let back: SessionRevertParams = serde_json::from_str(&json_str).unwrap();
1603 assert_eq!(params, back);
1604 }
1605
1606 #[test]
1607 fn file_part_with_symbol_source() {
1608 let part = Part::File(FilePart {
1609 id: "fp_sym".into(),
1610 message_id: "msg_001".into(),
1611 mime: "text/x-rust".into(),
1612 session_id: "sess_001".into(),
1613 url: "file:///src/lib.rs".into(),
1614 filename: Some("lib.rs".into()),
1615 source: Some(FilePartSource::Symbol(SymbolSource {
1616 kind: 6,
1617 name: "MyStruct".into(),
1618 path: "/src/lib.rs".into(),
1619 range: SymbolSourceRange {
1620 end: SymbolSourcePosition { character: 1, line: 10 },
1621 start: SymbolSourcePosition { character: 0, line: 5 },
1622 },
1623 text: FilePartSourceText {
1624 end: 200,
1625 start: 100,
1626 value: "struct MyStruct {}".into(),
1627 },
1628 })),
1629 });
1630 let json_str = serde_json::to_string(&part).unwrap();
1631 let v: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1632 assert_eq!(v["source"]["type"], "symbol");
1633 let back: Part = serde_json::from_str(&json_str).unwrap();
1634 assert_eq!(part, back);
1635 }
1636}