use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
pub mod transport {
pub const POPUP_PIPE: &str = "exomonad:popup";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupRequest {
pub request_id: String,
pub definition: PopupDefinition,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupResponse {
pub request_id: String,
pub result: PopupResult,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct AgentId(String);
impl TryFrom<String> for AgentId {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
if s.is_empty() {
return Err("Agent ID cannot be empty".to_string());
}
Ok(Self(s))
}
}
impl From<AgentId> for String {
fn from(id: AgentId) -> String {
id.0
}
}
impl fmt::Display for AgentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AgentEvent {
#[serde(rename = "agent:started")]
AgentStarted {
agent_id: AgentId,
timestamp: String,
},
#[serde(rename = "agent:stopped")]
AgentStopped {
agent_id: AgentId,
timestamp: String,
},
#[serde(rename = "stop_hook:blocked")]
StopHookBlocked {
agent_id: AgentId,
reason: String,
timestamp: String,
},
#[serde(rename = "hook:received")]
HookReceived {
agent_id: AgentId,
hook_type: String,
timestamp: String,
},
#[serde(rename = "pr:filed")]
PrFiled {
agent_id: AgentId,
pr_number: u64,
timestamp: String,
},
#[serde(rename = "copilot:reviewed")]
CopilotReviewed {
agent_id: AgentId,
comment_count: u32,
timestamp: String,
},
#[serde(rename = "agent:stuck")]
AgentStuck {
agent_id: AgentId,
failed_stop_count: u32,
timestamp: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupDefinition {
pub title: String,
pub components: Vec<Component>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum Component {
#[serde(rename = "text")]
Text {
id: String,
content: String,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "slider")]
Slider {
id: String,
label: String,
min: f32,
max: f32,
default: f32,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "checkbox")]
Checkbox {
id: String,
label: String,
default: bool,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "textbox")]
Textbox {
id: String,
label: String,
placeholder: Option<String>,
rows: Option<u32>,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "choice")]
Choice {
id: String,
label: String,
options: Vec<String>,
default: Option<usize>,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "multiselect")]
Multiselect {
id: String,
label: String,
options: Vec<String>,
default: Option<usize>, #[serde(default)]
visible_when: Option<VisibilityRule>,
},
#[serde(rename = "group")]
Group {
id: String,
label: String,
#[serde(default)]
visible_when: Option<VisibilityRule>,
},
}
impl Component {
pub fn id(&self) -> &str {
match self {
Component::Text { id, .. } => id,
Component::Slider { id, .. } => id,
Component::Checkbox { id, .. } => id,
Component::Textbox { id, .. } => id,
Component::Choice { id, .. } => id,
Component::Multiselect { id, .. } => id,
Component::Group { id, .. } => id,
}
}
pub fn visible_when(&self) -> Option<&VisibilityRule> {
match self {
Component::Text { visible_when, .. } => visible_when.as_ref(),
Component::Slider { visible_when, .. } => visible_when.as_ref(),
Component::Checkbox { visible_when, .. } => visible_when.as_ref(),
Component::Textbox { visible_when, .. } => visible_when.as_ref(),
Component::Choice { visible_when, .. } => visible_when.as_ref(),
Component::Multiselect { visible_when, .. } => visible_when.as_ref(),
Component::Group { visible_when, .. } => visible_when.as_ref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum VisibilityRule {
Checked(String),
Equals(HashMap<String, String>),
GreaterThan { id: String, min_value: f32 },
LessThan { id: String, max_value: f32 },
CountEquals { id: String, exact_count: u32 },
CountGreaterThan { id: String, min_count: u32 },
}
#[derive(Debug, Clone)]
pub struct PopupState {
pub values: HashMap<String, ElementValue>,
pub button_clicked: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ElementValue {
Number(f32),
Boolean(bool),
Text(String),
Choice(usize),
MultiChoice(Vec<bool>),
}
impl PopupState {
pub fn new(definition: &PopupDefinition) -> Self {
let mut values = HashMap::new();
for component in &definition.components {
match component {
Component::Slider { id, default, .. } => {
values.insert(id.clone(), ElementValue::Number(*default));
}
Component::Checkbox { id, default, .. } => {
values.insert(id.clone(), ElementValue::Boolean(*default));
}
Component::Textbox { id, .. } => {
values.insert(id.clone(), ElementValue::Text(String::new()));
}
Component::Choice { id, default, .. } => {
values.insert(id.clone(), ElementValue::Choice(default.unwrap_or(0)));
}
Component::Multiselect { id, options, .. } => {
values.insert(
id.clone(),
ElementValue::MultiChoice(vec![false; options.len()]),
);
}
_ => {}
}
}
Self {
values,
button_clicked: None,
}
}
pub fn to_json_values(&self) -> Value {
let mut map = serde_json::Map::new();
for (k, v) in &self.values {
let json_val = match v {
ElementValue::Number(n) => serde_json::json!(n),
ElementValue::Boolean(b) => serde_json::json!(b),
ElementValue::Text(s) => serde_json::json!(s),
ElementValue::Choice(i) => serde_json::json!(i),
ElementValue::MultiChoice(vec) => serde_json::json!(vec),
};
map.insert(k.clone(), json_val);
}
Value::Object(map)
}
pub fn get_number(&self, id: &str) -> Option<f32> {
match self.values.get(id) {
Some(ElementValue::Number(n)) => Some(*n),
_ => None,
}
}
pub fn get_boolean(&self, id: &str) -> Option<bool> {
match self.values.get(id) {
Some(ElementValue::Boolean(b)) => Some(*b),
_ => None,
}
}
pub fn get_text(&self, id: &str) -> Option<&str> {
match self.values.get(id) {
Some(ElementValue::Text(t)) => Some(t),
_ => None,
}
}
pub fn get_choice(&self, id: &str) -> Option<usize> {
match self.values.get(id) {
Some(ElementValue::Choice(c)) => Some(*c),
_ => None,
}
}
pub fn get_multichoice(&self, id: &str) -> Option<&[bool]> {
match self.values.get(id) {
Some(ElementValue::MultiChoice(v)) => Some(v),
_ => None,
}
}
pub fn set_number(&mut self, id: &str, value: f32) {
self.values
.insert(id.to_string(), ElementValue::Number(value));
}
pub fn set_boolean(&mut self, id: &str, value: bool) {
self.values
.insert(id.to_string(), ElementValue::Boolean(value));
}
pub fn set_text(&mut self, id: &str, value: String) {
self.values
.insert(id.to_string(), ElementValue::Text(value));
}
pub fn set_choice(&mut self, id: &str, value: usize) {
self.values
.insert(id.to_string(), ElementValue::Choice(value));
}
pub fn set_multichoice(&mut self, id: &str, value: Vec<bool>) {
self.values
.insert(id.to_string(), ElementValue::MultiChoice(value));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupResult {
pub button: String, pub values: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_spent_seconds: Option<f64>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "phase")]
pub enum CoordinatorAgentStatus {
#[serde(rename = "setting_up")]
SettingUp,
#[serde(rename = "pane_opening")]
PaneOpening,
#[serde(rename = "running")]
Running { pane_id: u32 },
#[serde(rename = "cleaning")]
Cleaning,
#[serde(rename = "completed")]
Completed { exit_code: i32 },
#[serde(rename = "failed")]
Failed { error: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorAgentState {
pub id: String,
pub worktree_path: String,
pub branch: String,
pub agent_type: String,
pub status: CoordinatorAgentStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum StateUpdate {
#[serde(rename = "coordinator:state")]
FullState { agents: Vec<CoordinatorAgentState> },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_component_deser() {
let json = r#"{
"id": "info",
"type": "text",
"content": "This is informational text"
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Text {
id,
content,
visible_when,
} => {
assert_eq!(id, "info");
assert_eq!(content, "This is informational text");
assert!(visible_when.is_none());
}
_ => panic!("Expected Text component"),
}
}
#[test]
fn test_slider_component_deser() {
let json = r#"{
"id": "confidence",
"type": "slider",
"label": "Confidence Level",
"min": 0.0,
"max": 100.0,
"default": 75.5
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Slider {
id,
label,
min,
max,
default,
visible_when,
} => {
assert_eq!(id, "confidence");
assert_eq!(label, "Confidence Level");
assert_eq!(min, 0.0);
assert_eq!(max, 100.0);
assert_eq!(default, 75.5);
assert!(visible_when.is_none());
}
_ => panic!("Expected Slider component"),
}
}
#[test]
fn test_checkbox_component_deser() {
let json = r#"{
"id": "agree",
"type": "checkbox",
"label": "I agree to the terms",
"default": false
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Checkbox {
id,
label,
default,
visible_when,
} => {
assert_eq!(id, "agree");
assert_eq!(label, "I agree to the terms");
assert!(!default);
assert!(visible_when.is_none());
}
_ => panic!("Expected Checkbox component"),
}
}
#[test]
fn test_textbox_component_deser() {
let json = r#"{
"id": "notes",
"type": "textbox",
"label": "Additional Notes",
"placeholder": "Enter notes here...",
"rows": 3
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Textbox {
id,
label,
placeholder,
rows,
visible_when,
} => {
assert_eq!(id, "notes");
assert_eq!(label, "Additional Notes");
assert_eq!(placeholder, Some("Enter notes here...".to_string()));
assert_eq!(rows, Some(3));
assert!(visible_when.is_none());
}
_ => panic!("Expected Textbox component"),
}
}
#[test]
fn test_choice_component_deser() {
let json = r#"{
"id": "color",
"type": "choice",
"label": "Choose a color",
"options": ["Red", "Green", "Blue"],
"default": 1
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Choice {
id,
label,
options,
default,
visible_when,
} => {
assert_eq!(id, "color");
assert_eq!(label, "Choose a color");
assert_eq!(options, vec!["Red", "Green", "Blue"]);
assert_eq!(default, Some(1));
assert!(visible_when.is_none());
}
_ => panic!("Expected Choice component"),
}
}
#[test]
fn test_multiselect_component_deser() {
let json = r#"{
"id": "features",
"type": "multiselect",
"label": "Select features",
"options": ["Feature A", "Feature B", "Feature C"]
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Multiselect {
id,
label,
options,
default,
visible_when,
} => {
assert_eq!(id, "features");
assert_eq!(label, "Select features");
assert_eq!(options, vec!["Feature A", "Feature B", "Feature C"]);
assert!(default.is_none());
assert!(visible_when.is_none());
}
_ => panic!("Expected Multiselect component"),
}
}
#[test]
fn test_group_component_deser() {
let json = r#"{
"id": "advanced",
"type": "group",
"label": "Advanced Settings"
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Group {
id,
label,
visible_when,
} => {
assert_eq!(id, "advanced");
assert_eq!(label, "Advanced Settings");
assert!(visible_when.is_none());
}
_ => panic!("Expected Group component"),
}
}
#[test]
fn test_component_with_visibility_deserialization() {
let json = r#"{
"id": "slider1",
"type": "slider",
"label": "My Slider",
"min": 0.0,
"max": 100.0,
"default": 50.0,
"visible_when": "checkbox1"
}"#;
let parsed: Component = serde_json::from_str(json).unwrap();
match parsed {
Component::Slider {
id, visible_when, ..
} => {
assert_eq!(id, "slider1");
assert_eq!(
visible_when,
Some(VisibilityRule::Checked("checkbox1".to_string()))
);
}
_ => panic!("Expected Slider component"),
}
}
#[test]
fn test_visibility_rule_variants() {
let json = r#"{"id": "s1", "min_value": 10.0}"#;
let rule: VisibilityRule = serde_json::from_str(json).unwrap();
match rule {
VisibilityRule::GreaterThan { id, min_value } => {
assert_eq!(id, "s1");
assert_eq!(min_value, 10.0);
}
_ => panic!("Expected GreaterThan"),
}
let json = r#"{"id": "m1", "exact_count": 2}"#;
let rule: VisibilityRule = serde_json::from_str(json).unwrap();
match rule {
VisibilityRule::CountEquals { id, exact_count } => {
assert_eq!(id, "m1");
assert_eq!(exact_count, 2);
}
_ => panic!("Expected CountEquals"),
}
}
#[test]
fn test_visibility_rule_less_than() {
let json = r#"{"id": "s1", "max_value": 50.0}"#;
let rule: VisibilityRule = serde_json::from_str(json).unwrap();
match rule {
VisibilityRule::LessThan { id, max_value } => {
assert_eq!(id, "s1");
assert_eq!(max_value, 50.0);
}
_ => panic!("Expected LessThan"),
}
}
#[test]
fn test_visibility_rule_count_greater_than() {
let json = r#"{"id": "m1", "min_count": 1}"#;
let rule: VisibilityRule = serde_json::from_str(json).unwrap();
match rule {
VisibilityRule::CountGreaterThan { id, min_count } => {
assert_eq!(id, "m1");
assert_eq!(min_count, 1);
}
_ => panic!("Expected CountGreaterThan"),
}
}
#[test]
fn test_popup_state_slider_default() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Slider {
id: "slider1".to_string(),
label: "Test Slider".to_string(),
min: 0.0,
max: 100.0,
default: 42.5,
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(state.get_number("slider1"), Some(42.5));
}
#[test]
fn test_popup_state_checkbox_default() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Checkbox {
id: "check1".to_string(),
label: "Test Checkbox".to_string(),
default: true,
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(state.get_boolean("check1"), Some(true));
}
#[test]
fn test_popup_state_textbox_empty() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Textbox {
id: "text1".to_string(),
label: "Test Textbox".to_string(),
placeholder: Some("Enter text...".to_string()),
rows: None,
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(state.get_text("text1"), Some(""));
}
#[test]
fn test_popup_state_choice_default() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Choice {
id: "choice1".to_string(),
label: "Test Choice".to_string(),
options: vec!["A".to_string(), "B".to_string(), "C".to_string()],
default: Some(2),
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(state.get_choice("choice1"), Some(2));
}
#[test]
fn test_popup_state_choice_no_default() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Choice {
id: "choice1".to_string(),
label: "Test Choice".to_string(),
options: vec!["A".to_string(), "B".to_string()],
default: None,
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(state.get_choice("choice1"), Some(0)); }
#[test]
fn test_popup_state_multiselect_init() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Multiselect {
id: "multi1".to_string(),
label: "Test Multi".to_string(),
options: vec!["X".to_string(), "Y".to_string(), "Z".to_string()],
default: None,
visible_when: None,
}],
};
let state = PopupState::new(&definition);
assert_eq!(
state.get_multichoice("multi1"),
Some(&[false, false, false][..])
);
}
#[test]
fn test_get_set_number() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Slider {
id: "num".to_string(),
label: "Number".to_string(),
min: 0.0,
max: 100.0,
default: 0.0,
visible_when: None,
}],
};
let mut state = PopupState::new(&definition);
assert_eq!(state.get_number("num"), Some(0.0));
state.set_number("num", 99.9);
assert_eq!(state.get_number("num"), Some(99.9));
}
#[test]
fn test_get_set_boolean() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Checkbox {
id: "flag".to_string(),
label: "Flag".to_string(),
default: false,
visible_when: None,
}],
};
let mut state = PopupState::new(&definition);
assert_eq!(state.get_boolean("flag"), Some(false));
state.set_boolean("flag", true);
assert_eq!(state.get_boolean("flag"), Some(true));
}
#[test]
fn test_get_set_text() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Textbox {
id: "txt".to_string(),
label: "Text".to_string(),
placeholder: None,
rows: None,
visible_when: None,
}],
};
let mut state = PopupState::new(&definition);
assert_eq!(state.get_text("txt"), Some(""));
state.set_text("txt", "Hello World".to_string());
assert_eq!(state.get_text("txt"), Some("Hello World"));
}
#[test]
fn test_get_set_choice() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Choice {
id: "sel".to_string(),
label: "Selection".to_string(),
options: vec!["A".to_string(), "B".to_string(), "C".to_string()],
default: Some(0),
visible_when: None,
}],
};
let mut state = PopupState::new(&definition);
assert_eq!(state.get_choice("sel"), Some(0));
state.set_choice("sel", 2);
assert_eq!(state.get_choice("sel"), Some(2));
}
#[test]
fn test_get_set_multichoice() {
let definition = PopupDefinition {
title: "Test".to_string(),
components: vec![Component::Multiselect {
id: "opts".to_string(),
label: "Options".to_string(),
options: vec!["1".to_string(), "2".to_string(), "3".to_string()],
default: None,
visible_when: None,
}],
};
let mut state = PopupState::new(&definition);
assert_eq!(
state.get_multichoice("opts"),
Some(&[false, false, false][..])
);
state.set_multichoice("opts", vec![true, false, true]);
assert_eq!(
state.get_multichoice("opts"),
Some(&[true, false, true][..])
);
}
#[test]
fn test_component_id_method() {
let text = Component::Text {
id: "t1".to_string(),
content: "Hello".to_string(),
visible_when: None,
};
assert_eq!(text.id(), "t1");
let slider = Component::Slider {
id: "s1".to_string(),
label: "Slider".to_string(),
min: 0.0,
max: 100.0,
default: 50.0,
visible_when: None,
};
assert_eq!(slider.id(), "s1");
}
#[test]
fn test_component_visible_when_method() {
let component = Component::Checkbox {
id: "cb".to_string(),
label: "Check".to_string(),
default: false,
visible_when: Some(VisibilityRule::Checked("other".to_string())),
};
assert_eq!(
component.visible_when(),
Some(&VisibilityRule::Checked("other".to_string()))
);
let no_rule = Component::Text {
id: "t".to_string(),
content: "x".to_string(),
visible_when: None,
};
assert!(no_rule.visible_when().is_none());
}
#[test]
fn test_to_json_values() {
let mut values = HashMap::new();
values.insert("num".to_string(), ElementValue::Number(42.0));
values.insert("bool".to_string(), ElementValue::Boolean(true));
values.insert("text".to_string(), ElementValue::Text("hello".to_string()));
values.insert("choice".to_string(), ElementValue::Choice(1));
values.insert(
"multi".to_string(),
ElementValue::MultiChoice(vec![true, false]),
);
let state = PopupState {
values,
button_clicked: None,
};
let json = state.to_json_values();
let obj = json.as_object().unwrap();
assert_eq!(obj["num"], 42.0);
assert_eq!(obj["bool"], true);
assert_eq!(obj["text"], "hello");
assert_eq!(obj["choice"], 1);
assert_eq!(obj["multi"][0], true);
assert_eq!(obj["multi"][1], false);
}
#[test]
fn test_agent_id_valid() {
let id: Result<AgentId, _> = "agent-123".to_string().try_into();
assert!(id.is_ok());
assert_eq!(id.unwrap().to_string(), "agent-123");
}
#[test]
fn test_agent_id_empty_rejected() {
let id: Result<AgentId, _> = "".to_string().try_into();
assert!(id.is_err());
assert!(id.unwrap_err().contains("empty"));
}
#[test]
fn test_agent_id_serialization_roundtrip() {
let id: AgentId = "test-agent".to_string().try_into().unwrap();
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"test-agent\"");
let deserialized: AgentId = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.to_string(), "test-agent");
}
#[test]
fn test_agent_event_serialization() {
let event = AgentEvent::AgentStarted {
agent_id: "agent-1".to_string().try_into().unwrap(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"agent:started\""));
assert!(json.contains("\"agent_id\":\"agent-1\""));
}
#[test]
fn test_popup_result_serialization() {
let result = PopupResult {
button: "submit".to_string(),
values: serde_json::json!({"name": "test"}),
time_spent_seconds: Some(5.5),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"button\":\"submit\""));
assert!(json.contains("\"time_spent_seconds\":5.5"));
let deserialized: PopupResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.button, "submit");
}
#[test]
fn test_popup_request_serialization() {
let request = super::PopupRequest {
request_id: "req-123".to_string(),
definition: PopupDefinition {
title: "Test Form".to_string(),
components: vec![Component::Text {
id: "msg".to_string(),
content: "Hello".to_string(),
visible_when: None,
}],
},
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"request_id\":\"req-123\""));
assert!(json.contains("\"title\":\"Test Form\""));
let deserialized: super::PopupRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.request_id, "req-123");
assert_eq!(deserialized.definition.title, "Test Form");
}
#[test]
fn test_popup_response_serialization() {
let response = super::PopupResponse {
request_id: "req-456".to_string(),
result: PopupResult {
button: "submit".to_string(),
values: serde_json::json!({"name": "test"}),
time_spent_seconds: Some(10.0),
},
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"request_id\":\"req-456\""));
assert!(json.contains("\"button\":\"submit\""));
let deserialized: super::PopupResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.request_id, "req-456");
assert_eq!(deserialized.result.button, "submit");
}
#[test]
fn test_transport_constants() {
assert_eq!(super::transport::POPUP_PIPE, "exomonad:popup");
}
}