#![cfg_attr(test, allow(clippy::expect_used))]
use serde::ser::Serializer;
use serde::Serialize;
use crate::events::{Command, UiEvent};
pub const PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Serialize)]
pub struct SessionHeader {
#[serde(rename = "type")]
kind: &'static str,
pub protocol_version: u32,
pub session_id: String,
pub cwd: String,
}
impl SessionHeader {
pub fn new(session_id: String, cwd: String) -> Self {
Self {
kind: "session",
protocol_version: PROTOCOL_VERSION,
session_id,
cwd,
}
}
}
#[derive(Debug)]
pub struct ProtocolError(pub String);
impl std::fmt::Display for ProtocolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ProtocolError {}
pub fn decode_command(line: &str) -> Result<Command, ProtocolError> {
serde_json::from_str(line).map_err(|e| ProtocolError(e.to_string()))
}
pub fn encode_event(event: &UiEvent) -> String {
serde_json::to_string(event)
.unwrap_or_else(|e| format!(r#"{{"type":"error","payload":"event encode failed: {e}"}}"#))
}
pub fn serialize_branch_tree<S>(
tree: &motosan_agent_loop::BranchTree,
s: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
struct WireNode {
id: String,
parent: Option<usize>,
children: Vec<usize>,
label: String,
}
#[derive(Serialize)]
struct WireTree {
nodes: Vec<WireNode>,
root: Option<usize>,
active_leaf: Option<usize>,
}
let wire = WireTree {
nodes: tree
.nodes
.iter()
.map(|n| WireNode {
id: n.id.clone(),
parent: n.parent,
children: n.children.clone(),
label: n.label.clone(),
})
.collect(),
root: tree.root,
active_leaf: tree.active_leaf,
};
wire.serialize(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_header_serializes_with_version() {
let h = SessionHeader::new("sess-1".into(), "/tmp/x".into());
let line = serde_json::to_string(&h).expect("serialize");
assert!(line.contains(r#""type":"session""#), "{line}");
assert!(line.contains(r#""protocol_version":1"#), "{line}");
assert!(line.contains(r#""session_id":"sess-1""#), "{line}");
}
#[test]
fn decode_command_parses_a_valid_line() {
let cmd = decode_command(r#"{"type":"cancel_agent"}"#).expect("decode");
assert_eq!(cmd, crate::events::Command::CancelAgent);
}
#[test]
fn decode_command_rejects_garbage() {
let err = decode_command("not json").expect_err("should fail");
assert!(!err.to_string().is_empty());
}
#[test]
fn serialize_branch_tree_emits_nodes() {
use motosan_agent_loop::{BranchNode, BranchTree};
let tree = BranchTree {
nodes: vec![BranchNode {
id: "n0".into(),
parent: None,
children: vec![],
label: "root".into(),
}],
root: Some(0),
active_leaf: Some(0),
};
#[derive(serde::Serialize)]
struct W<'a>(#[serde(serialize_with = "serialize_branch_tree")] &'a BranchTree);
let line = serde_json::to_string(&W(&tree)).expect("serialize");
assert!(line.contains(r#""label":"root""#), "{line}");
assert!(line.contains(r#""active_leaf":0"#), "{line}");
}
#[test]
fn encode_event_produces_one_tagged_line() {
use crate::events::UiEvent;
assert_eq!(
encode_event(&UiEvent::AgentThinking),
r#"{"type":"agent_thinking"}"#
);
let line = encode_event(&UiEvent::AgentTextDelta("hi".into()));
assert_eq!(line, r#"{"type":"agent_text_delta","payload":"hi"}"#);
assert!(!line.contains('\n'), "encode_event must not add a newline");
}
}