1use crate::activations::arbor::{NodeId, TreeId};
2use plexus_macros::HandleEnum;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7use uuid::Uuid;
8
9use super::activation::ClaudeCode;
10
11pub type ClaudeCodeId = Uuid;
13
14#[derive(Debug, Clone, HandleEnum)]
23#[handle(plugin_id = "ClaudeCode::PLUGIN_ID", version = "1.0.0")]
24pub enum ClaudeCodeHandle {
25 #[handle(
28 method = "chat",
29 table = "messages",
30 key = "id",
31 key_field = "message_id",
32 strip_prefix = "msg-"
33 )]
34 Message {
35 message_id: String,
37 role: String,
39 name: String,
41 },
42
43 #[handle(method = "passthrough")]
47 Passthrough {
48 event_id: String,
50 event_type: String,
52 },
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
61#[serde(tag = "type")]
62pub enum ResolveResult {
63 #[serde(rename = "resolved_message")]
65 Message {
66 id: String,
67 role: String,
68 content: String,
69 model: Option<String>,
70 name: String,
71 },
72 #[serde(rename = "error")]
74 Error { message: String },
75}
76
77pub type StreamId = Uuid;
79
80pub type MessageId = Uuid;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(rename_all = "snake_case")]
86pub enum MessageRole {
87 User,
88 Assistant,
89 System,
90}
91
92impl MessageRole {
93 pub fn as_str(&self) -> &'static str {
94 match self {
95 MessageRole::User => "user",
96 MessageRole::Assistant => "assistant",
97 MessageRole::System => "system",
98 }
99 }
100
101 pub fn from_str(s: &str) -> Option<Self> {
102 match s {
103 "user" => Some(MessageRole::User),
104 "assistant" => Some(MessageRole::Assistant),
105 "system" => Some(MessageRole::System),
106 _ => None,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
113#[serde(rename_all = "lowercase")]
114pub enum Model {
115 Opus,
116 Sonnet,
117 Haiku,
118}
119
120impl Model {
121 pub fn as_str(&self) -> &'static str {
122 match self {
123 Model::Opus => "opus",
124 Model::Sonnet => "sonnet",
125 Model::Haiku => "haiku",
126 }
127 }
128
129 pub fn from_str(s: &str) -> Option<Self> {
130 match s.to_lowercase().as_str() {
131 "opus" => Some(Model::Opus),
132 "sonnet" => Some(Model::Sonnet),
133 "haiku" => Some(Model::Haiku),
134 _ => None,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
142pub struct Position {
143 pub tree_id: TreeId,
145 pub node_id: NodeId,
147}
148
149impl Position {
150 pub fn new(tree_id: TreeId, node_id: NodeId) -> Self {
152 Self { tree_id, node_id }
153 }
154
155 pub fn advance(&self, new_node_id: NodeId) -> Self {
157 Self {
158 tree_id: self.tree_id,
159 node_id: new_node_id,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
166pub struct Message {
167 pub id: MessageId,
168 pub session_id: ClaudeCodeId,
169 pub role: MessageRole,
170 pub content: String,
171 pub created_at: i64,
172 pub model_id: Option<String>,
174 pub input_tokens: Option<i64>,
176 pub output_tokens: Option<i64>,
177 pub cost_usd: Option<f64>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
183pub struct ClaudeCodeConfig {
184 pub id: ClaudeCodeId,
186 pub name: String,
188 pub claude_session_id: Option<String>,
190 pub loopback_session_id: Option<String>,
192 pub head: Position,
194 pub working_dir: String,
196 pub model: Model,
198 pub system_prompt: Option<String>,
200 pub mcp_config: Option<Value>,
202 pub loopback_enabled: bool,
204 pub metadata: Option<Value>,
206 pub created_at: i64,
208 pub updated_at: i64,
210}
211
212impl ClaudeCodeConfig {
213 pub fn tree_id(&self) -> TreeId {
215 self.head.tree_id
216 }
217
218 pub fn node_id(&self) -> NodeId {
220 self.head.node_id
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226pub struct ClaudeCodeInfo {
227 pub id: ClaudeCodeId,
228 pub name: String,
229 pub model: Model,
230 pub head: Position,
231 pub claude_session_id: Option<String>,
232 pub working_dir: String,
233 pub loopback_enabled: bool,
234 pub created_at: i64,
235}
236
237impl From<&ClaudeCodeConfig> for ClaudeCodeInfo {
238 fn from(config: &ClaudeCodeConfig) -> Self {
239 Self {
240 id: config.id,
241 name: config.name.clone(),
242 model: config.model,
243 head: config.head,
244 claude_session_id: config.claude_session_id.clone(),
245 working_dir: config.working_dir.clone(),
246 loopback_enabled: config.loopback_enabled,
247 created_at: config.created_at,
248 }
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
254pub struct ChatUsage {
255 pub input_tokens: Option<u64>,
256 pub output_tokens: Option<u64>,
257 pub cost_usd: Option<f64>,
258 pub num_turns: Option<i32>,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
267#[serde(rename_all = "snake_case")]
268pub enum StreamStatus {
269 Running,
271 AwaitingPermission,
273 Complete,
275 Failed,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
281pub struct StreamInfo {
282 pub stream_id: StreamId,
284 pub session_id: ClaudeCodeId,
286 pub status: StreamStatus,
288 pub user_position: Option<Position>,
290 pub event_count: u64,
292 pub read_position: u64,
294 pub started_at: i64,
296 pub ended_at: Option<i64>,
298 pub error: Option<String>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
304pub struct BufferedEvent {
305 pub seq: u64,
307 pub event: ChatEvent,
309 pub timestamp: i64,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
320#[serde(tag = "type", rename_all = "snake_case")]
321pub enum CreateResult {
322 #[serde(rename = "created")]
323 Ok {
324 id: ClaudeCodeId,
325 head: Position,
326 },
327 #[serde(rename = "error")]
328 Err { message: String },
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
333#[serde(tag = "type", rename_all = "snake_case")]
334pub enum GetResult {
335 #[serde(rename = "ok")]
336 Ok { config: ClaudeCodeConfig },
337 #[serde(rename = "error")]
338 Err { message: String },
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
343#[serde(tag = "type", rename_all = "snake_case")]
344pub enum ListResult {
345 #[serde(rename = "ok")]
346 Ok { sessions: Vec<ClaudeCodeInfo> },
347 #[serde(rename = "error")]
348 Err { message: String },
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
353#[serde(tag = "type", rename_all = "snake_case")]
354pub enum DeleteResult {
355 #[serde(rename = "deleted")]
356 Ok { id: ClaudeCodeId },
357 #[serde(rename = "error")]
358 Err { message: String },
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
363#[serde(tag = "type", rename_all = "snake_case")]
364pub enum ForkResult {
365 #[serde(rename = "forked")]
366 Ok {
367 id: ClaudeCodeId,
368 head: Position,
369 },
370 #[serde(rename = "error")]
371 Err { message: String },
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
376#[serde(tag = "type", rename_all = "snake_case")]
377pub enum ChatStartResult {
378 #[serde(rename = "started")]
379 Ok {
380 stream_id: StreamId,
381 session_id: ClaudeCodeId,
382 },
383 #[serde(rename = "error")]
384 Err { message: String },
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
389#[serde(tag = "type", rename_all = "snake_case")]
390pub enum PollResult {
391 #[serde(rename = "ok")]
392 Ok {
393 status: StreamStatus,
395 events: Vec<BufferedEvent>,
397 read_position: u64,
399 total_events: u64,
401 has_more: bool,
403 },
404 #[serde(rename = "error")]
405 Err { message: String },
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
410#[serde(tag = "type", rename_all = "snake_case")]
411pub enum StreamListResult {
412 #[serde(rename = "ok")]
413 Ok { streams: Vec<StreamInfo> },
414 #[serde(rename = "error")]
415 Err { message: String },
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
424#[serde(tag = "type", rename_all = "snake_case")]
425pub enum ChatEvent {
426 #[serde(rename = "start")]
428 Start {
429 id: ClaudeCodeId,
430 user_position: Position,
431 },
432
433 #[serde(rename = "content")]
435 Content { text: String },
436
437 #[serde(rename = "thinking")]
439 Thinking { thinking: String },
440
441 #[serde(rename = "tool_use")]
443 ToolUse {
444 tool_name: String,
445 tool_use_id: String,
446 input: Value,
447 },
448
449 #[serde(rename = "tool_result")]
451 ToolResult {
452 tool_use_id: String,
453 output: String,
454 is_error: bool,
455 },
456
457 #[serde(rename = "complete")]
459 Complete {
460 new_head: Position,
461 claude_session_id: String,
462 usage: Option<ChatUsage>,
463 },
464
465 #[serde(rename = "passthrough")]
468 Passthrough {
469 event_type: String,
470 handle: String,
471 data: Value,
472 },
473
474 #[serde(rename = "error")]
476 Err { message: String },
477}
478
479#[derive(Debug, Error)]
481pub enum ClaudeCodeError {
482 #[error("failed to resolve working directory '{path}': {source}")]
483 PathResolution { path: String, source: std::io::Error },
484
485 #[error("session not found: {identifier}")]
486 SessionNotFound { identifier: String },
487
488 #[error("ambiguous session name '{name}' matches multiple sessions: {matches}")]
489 AmbiguousSession { name: String, matches: String },
490
491 #[error("database error: {operation}: {source}")]
492 Database { operation: &'static str, source: sqlx::Error },
493
494 #[error("parse error: {context}: {detail}")]
495 Parse { context: &'static str, detail: String },
496
497 #[error("serialization error: {0}")]
498 Serialization(#[from] serde_json::Error),
499
500 #[error("arbor error: {0}")]
501 Arbor(String),
502}
503
504#[derive(Debug, Clone, Deserialize)]
510#[serde(tag = "type")]
511pub enum RawClaudeEvent {
512 #[serde(rename = "system")]
514 System {
515 subtype: Option<String>,
516 #[serde(rename = "session_id")]
517 session_id: Option<String>,
518 model: Option<String>,
519 cwd: Option<String>,
520 tools: Option<Vec<String>>,
521 },
522
523 #[serde(rename = "assistant")]
525 Assistant {
526 message: Option<RawMessage>,
527 },
528
529 #[serde(rename = "user")]
531 User {
532 message: Option<RawMessage>,
533 },
534
535 #[serde(rename = "result")]
537 Result {
538 subtype: Option<String>,
539 session_id: Option<String>,
540 cost_usd: Option<f64>,
541 is_error: Option<bool>,
542 duration_ms: Option<i64>,
543 num_turns: Option<i32>,
544 result: Option<String>,
545 error: Option<String>,
546 },
547
548 #[serde(rename = "stream_event")]
550 StreamEvent {
551 event: StreamEventInner,
552 session_id: Option<String>,
553 },
554
555 #[serde(skip)]
558 Unknown {
559 event_type: String,
560 data: Value,
561 },
562
563 #[serde(skip)]
565 LaunchCommand { command: String },
566
567 #[serde(skip)]
569 Stderr { text: String },
570}
571
572#[derive(Debug, Clone, Deserialize)]
574#[serde(tag = "type")]
575pub enum StreamEventInner {
576 #[serde(rename = "message_start")]
577 MessageStart {
578 message: Option<StreamMessage>,
579 },
580
581 #[serde(rename = "content_block_start")]
582 ContentBlockStart {
583 index: usize,
584 content_block: Option<StreamContentBlock>,
585 },
586
587 #[serde(rename = "content_block_delta")]
588 ContentBlockDelta {
589 index: usize,
590 delta: StreamDelta,
591 },
592
593 #[serde(rename = "content_block_stop")]
594 ContentBlockStop {
595 index: usize,
596 },
597
598 #[serde(rename = "message_delta")]
599 MessageDelta {
600 delta: MessageDeltaInfo,
601 },
602
603 #[serde(rename = "message_stop")]
604 MessageStop,
605}
606
607#[derive(Debug, Clone, Deserialize)]
608pub struct StreamMessage {
609 pub model: Option<String>,
610 pub role: Option<String>,
611}
612
613#[derive(Debug, Clone, Deserialize)]
614#[serde(tag = "type")]
615pub enum StreamContentBlock {
616 #[serde(rename = "text")]
617 Text { text: Option<String> },
618
619 #[serde(rename = "tool_use")]
620 ToolUse {
621 id: String,
622 name: String,
623 input: Option<Value>,
624 },
625}
626
627#[derive(Debug, Clone, Deserialize)]
628#[serde(tag = "type")]
629pub enum StreamDelta {
630 #[serde(rename = "text_delta")]
631 TextDelta { text: String },
632
633 #[serde(rename = "input_json_delta")]
634 InputJsonDelta { partial_json: String },
635}
636
637#[derive(Debug, Clone, Deserialize)]
638pub struct MessageDeltaInfo {
639 pub stop_reason: Option<String>,
640 pub stop_sequence: Option<String>,
641}
642
643#[derive(Debug, Clone, Deserialize)]
644pub struct RawMessage {
645 pub id: Option<String>,
646 pub role: Option<String>,
647 pub model: Option<String>,
648 pub content: Option<Vec<RawContentBlock>>,
649}
650
651#[derive(Debug, Clone, Deserialize)]
652#[serde(tag = "type")]
653pub enum RawContentBlock {
654 #[serde(rename = "text")]
655 Text { text: String },
656
657 #[serde(rename = "thinking")]
658 Thinking {
659 thinking: String,
660 #[serde(default)]
661 signature: Option<String>,
662 },
663
664 #[serde(rename = "tool_use")]
665 ToolUse {
666 id: String,
667 name: String,
668 input: Value,
669 },
670
671 #[serde(rename = "tool_result")]
672 ToolResult {
673 tool_use_id: String,
674 content: Option<String>,
675 is_error: Option<bool>,
676 },
677}
678
679#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
688#[serde(tag = "type", rename_all = "snake_case")]
689pub enum NodeEvent {
690 #[serde(rename = "user_message")]
692 UserMessage { content: String },
693
694 #[serde(rename = "assistant_start")]
696 AssistantStart,
697
698 #[serde(rename = "content_text")]
700 ContentText { text: String },
701
702 #[serde(rename = "content_tool_use")]
704 ContentToolUse {
705 id: String,
706 name: String,
707 input: Value,
708 },
709
710 #[serde(rename = "content_thinking")]
712 ContentThinking { thinking: String },
713
714 #[serde(rename = "user_tool_result")]
716 UserToolResult {
717 tool_use_id: String,
718 content: String,
719 is_error: bool,
720 },
721
722 #[serde(rename = "assistant_complete")]
724 AssistantComplete { usage: Option<ChatUsage> },
725
726 #[serde(rename = "launch_command")]
728 LaunchCommand { command: String },
729
730 #[serde(rename = "claude_stderr")]
732 ClaudeStderr { text: String },
733}
734
735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
737pub struct ClaudeMessage {
738 pub role: String,
740 pub content: Vec<ContentBlock>,
742}
743
744#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
746#[serde(tag = "type", rename_all = "snake_case")]
747pub enum ContentBlock {
748 #[serde(rename = "text")]
750 Text { text: String },
751
752 #[serde(rename = "tool_use")]
754 ToolUse {
755 id: String,
756 name: String,
757 input: Value,
758 },
759
760 #[serde(rename = "tool_result")]
762 ToolResult {
763 tool_use_id: String,
764 content: String,
765 is_error: bool,
766 },
767
768 #[serde(rename = "thinking")]
770 Thinking { thinking: String },
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
775#[serde(tag = "type", rename_all = "snake_case")]
776pub enum RenderResult {
777 #[serde(rename = "ok")]
778 Ok { messages: Vec<ClaudeMessage> },
779 #[serde(rename = "error")]
780 Err { message: String },
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
785#[serde(tag = "type", rename_all = "snake_case")]
786pub enum GetTreeResult {
787 #[serde(rename = "ok")]
788 Ok { tree_id: TreeId, head: NodeId },
789 #[serde(rename = "error")]
790 Err { message: String },
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
799#[serde(tag = "type", rename_all = "snake_case")]
800pub enum SessionsListResult {
801 #[serde(rename = "ok")]
802 Ok { sessions: Vec<String> },
803 #[serde(rename = "error")]
804 Err { message: String },
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
809#[serde(tag = "type", rename_all = "snake_case")]
810pub enum SessionsGetResult {
811 #[serde(rename = "ok")]
812 Ok {
813 session_id: String,
814 event_count: usize,
815 events: Vec<serde_json::Value>,
816 },
817 #[serde(rename = "error")]
818 Err { message: String },
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
823#[serde(tag = "type", rename_all = "snake_case")]
824pub enum SessionsImportResult {
825 #[serde(rename = "ok")]
826 Ok { tree_id: TreeId, session_id: String },
827 #[serde(rename = "error")]
828 Err { message: String },
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
833#[serde(tag = "type", rename_all = "snake_case")]
834pub enum SessionsExportResult {
835 #[serde(rename = "ok")]
836 Ok { tree_id: TreeId, session_id: String },
837 #[serde(rename = "error")]
838 Err { message: String },
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
843#[serde(tag = "type", rename_all = "snake_case")]
844pub enum SessionsDeleteResult {
845 #[serde(rename = "ok")]
846 Ok { session_id: String, deleted: bool },
847 #[serde(rename = "error")]
848 Err { message: String },
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 #[test]
856 fn test_node_event_serialization() {
857 let event = NodeEvent::ContentText {
858 text: "Hello".to_string(),
859 };
860 let json = serde_json::to_string(&event).unwrap();
861 let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
862 assert_eq!(event, parsed);
863 }
864
865 #[test]
866 fn test_claude_message_structure() {
867 let msg = ClaudeMessage {
868 role: "user".to_string(),
869 content: vec![ContentBlock::Text {
870 text: "test".to_string(),
871 }],
872 };
873 let json = serde_json::to_value(&msg).unwrap();
874 assert_eq!(json["role"], "user");
875 assert_eq!(json["content"][0]["type"], "text");
876 }
877
878 #[test]
879 fn test_json_schema_generation() {
880 use schemars::schema_for;
881
882 let _schema = schema_for!(NodeEvent);
884 let _schema = schema_for!(ClaudeMessage);
885 let _schema = schema_for!(ContentBlock);
886 let _schema = schema_for!(RenderResult);
887 let _schema = schema_for!(GetTreeResult);
888 }
889
890 #[test]
891 fn test_all_node_event_variants() {
892 let events = vec![
894 NodeEvent::UserMessage {
895 content: "Hello".to_string(),
896 },
897 NodeEvent::AssistantStart,
898 NodeEvent::ContentText {
899 text: "Response".to_string(),
900 },
901 NodeEvent::ContentToolUse {
902 id: "tool_123".to_string(),
903 name: "Write".to_string(),
904 input: serde_json::json!({"file": "test.txt"}),
905 },
906 NodeEvent::ContentThinking {
907 thinking: "Let me think...".to_string(),
908 },
909 NodeEvent::UserToolResult {
910 tool_use_id: "tool_123".to_string(),
911 content: "Success".to_string(),
912 is_error: false,
913 },
914 NodeEvent::AssistantComplete {
915 usage: Some(ChatUsage {
916 input_tokens: Some(100),
917 output_tokens: Some(200),
918 cost_usd: Some(0.01),
919 num_turns: Some(1),
920 }),
921 },
922 ];
923
924 for event in events {
925 let json = serde_json::to_string(&event).unwrap();
926 let parsed: NodeEvent = serde_json::from_str(&json).unwrap();
927 assert_eq!(event, parsed);
928 }
929 }
930
931 #[test]
932 fn test_all_content_block_variants() {
933 let blocks = vec![
935 ContentBlock::Text {
936 text: "Hello".to_string(),
937 },
938 ContentBlock::ToolUse {
939 id: "tool_456".to_string(),
940 name: "Bash".to_string(),
941 input: serde_json::json!({"command": "ls"}),
942 },
943 ContentBlock::ToolResult {
944 tool_use_id: "tool_456".to_string(),
945 content: "file1.txt\nfile2.txt".to_string(),
946 is_error: false,
947 },
948 ContentBlock::Thinking {
949 thinking: "Analyzing...".to_string(),
950 },
951 ];
952
953 for block in blocks {
954 let json = serde_json::to_string(&block).unwrap();
955 let parsed: ContentBlock = serde_json::from_str(&json).unwrap();
956 assert_eq!(block, parsed);
957 }
958 }
959
960 #[test]
961 fn test_node_event_json_format() {
962 let event = NodeEvent::ContentToolUse {
964 id: "toolu_123".to_string(),
965 name: "Write".to_string(),
966 input: serde_json::json!({"path": "/tmp/test.txt"}),
967 };
968 let json = serde_json::to_value(&event).unwrap();
969 assert_eq!(json["type"], "content_tool_use");
970 assert_eq!(json["id"], "toolu_123");
971 assert_eq!(json["name"], "Write");
972 assert_eq!(json["input"]["path"], "/tmp/test.txt");
973 }
974
975 #[test]
976 fn test_render_result_variants() {
977 let result = RenderResult::Ok {
979 messages: vec![ClaudeMessage {
980 role: "user".to_string(),
981 content: vec![ContentBlock::Text {
982 text: "test".to_string(),
983 }],
984 }],
985 };
986 let json = serde_json::to_value(&result).unwrap();
987 assert_eq!(json["type"], "ok");
988 assert!(json["messages"].is_array());
989
990 let result = RenderResult::Err {
992 message: "test error".to_string(),
993 };
994 let json = serde_json::to_value(&result).unwrap();
995 assert_eq!(json["type"], "error");
996 assert_eq!(json["message"], "test error");
997 }
998
999 #[test]
1000 fn test_get_tree_result_variants() {
1001 use crate::activations::arbor::{NodeId, TreeId};
1002
1003 let tree_id = TreeId::new();
1005 let node_id = NodeId::new();
1006 let result = GetTreeResult::Ok {
1007 tree_id,
1008 head: node_id,
1009 };
1010 let json = serde_json::to_value(&result).unwrap();
1011 assert_eq!(json["type"], "ok");
1012
1013 let result = GetTreeResult::Err {
1015 message: "not found".to_string(),
1016 };
1017 let json = serde_json::to_value(&result).unwrap();
1018 assert_eq!(json["type"], "error");
1019 assert_eq!(json["message"], "not found");
1020 }
1021}