1#![cfg_attr(test, allow(clippy::expect_used))]
2
3use serde::ser::Serializer;
8use serde::Serialize;
9
10use crate::events::{Command, UiEvent};
11
12pub const PROTOCOL_VERSION: u32 = 1;
14
15#[derive(Debug, Serialize)]
17pub struct SessionHeader {
18 #[serde(rename = "type")]
19 kind: &'static str,
20 pub protocol_version: u32,
21 pub session_id: String,
22 pub cwd: String,
23}
24
25impl SessionHeader {
26 pub fn new(session_id: String, cwd: String) -> Self {
27 Self {
28 kind: "session",
29 protocol_version: PROTOCOL_VERSION,
30 session_id,
31 cwd,
32 }
33 }
34}
35
36#[derive(Debug)]
38pub struct ProtocolError(pub String);
39
40impl std::fmt::Display for ProtocolError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46impl std::error::Error for ProtocolError {}
47
48pub fn decode_command(line: &str) -> Result<Command, ProtocolError> {
50 serde_json::from_str(line).map_err(|e| ProtocolError(e.to_string()))
51}
52
53pub fn encode_event(event: &UiEvent) -> String {
58 serde_json::to_string(event)
59 .unwrap_or_else(|e| format!(r#"{{"type":"error","payload":"event encode failed: {e}"}}"#))
60}
61
62pub fn serialize_branch_tree<S>(
66 tree: &motosan_agent_loop::BranchTree,
67 s: S,
68) -> Result<S::Ok, S::Error>
69where
70 S: Serializer,
71{
72 #[derive(Serialize)]
73 struct WireNode {
74 id: String,
75 parent: Option<usize>,
76 children: Vec<usize>,
77 label: String,
78 }
79 #[derive(Serialize)]
80 struct WireTree {
81 nodes: Vec<WireNode>,
82 root: Option<usize>,
83 active_leaf: Option<usize>,
84 }
85 let wire = WireTree {
86 nodes: tree
87 .nodes
88 .iter()
89 .map(|n| WireNode {
90 id: n.id.clone(),
91 parent: n.parent,
92 children: n.children.clone(),
93 label: n.label.clone(),
94 })
95 .collect(),
96 root: tree.root,
97 active_leaf: tree.active_leaf,
98 };
99 wire.serialize(s)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn session_header_serializes_with_version() {
108 let h = SessionHeader::new("sess-1".into(), "/tmp/x".into());
109 let line = serde_json::to_string(&h).expect("serialize");
110 assert!(line.contains(r#""type":"session""#), "{line}");
111 assert!(line.contains(r#""protocol_version":1"#), "{line}");
112 assert!(line.contains(r#""session_id":"sess-1""#), "{line}");
113 }
114
115 #[test]
116 fn decode_command_parses_a_valid_line() {
117 let cmd = decode_command(r#"{"type":"cancel_agent"}"#).expect("decode");
118 assert_eq!(cmd, crate::events::Command::CancelAgent);
119 }
120
121 #[test]
122 fn decode_command_rejects_garbage() {
123 let err = decode_command("not json").expect_err("should fail");
124 assert!(!err.to_string().is_empty());
125 }
126
127 #[test]
128 fn serialize_branch_tree_emits_nodes() {
129 use motosan_agent_loop::{BranchNode, BranchTree};
130 let tree = BranchTree {
131 nodes: vec![BranchNode {
132 id: "n0".into(),
133 parent: None,
134 children: vec![],
135 label: "root".into(),
136 }],
137 root: Some(0),
138 active_leaf: Some(0),
139 };
140 #[derive(serde::Serialize)]
142 struct W<'a>(#[serde(serialize_with = "serialize_branch_tree")] &'a BranchTree);
143 let line = serde_json::to_string(&W(&tree)).expect("serialize");
144 assert!(line.contains(r#""label":"root""#), "{line}");
145 assert!(line.contains(r#""active_leaf":0"#), "{line}");
146 }
147
148 #[test]
149 fn encode_event_produces_one_tagged_line() {
150 use crate::events::UiEvent;
151 assert_eq!(
152 encode_event(&UiEvent::AgentThinking),
153 r#"{"type":"agent_thinking"}"#
154 );
155 let line = encode_event(&UiEvent::AgentTextDelta("hi".into()));
156 assert_eq!(line, r#"{"type":"agent_text_delta","payload":"hi"}"#);
157 assert!(!line.contains('\n'), "encode_event must not add a newline");
158 }
159}