1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use super::oref::ORef;
12
13fn serialize_meta_as_null_if_empty<S>(meta: &MetaMapType, serializer: S) -> Result<S::Ok, S::Error>
19where
20 S: serde::Serializer,
21{
22 if meta.is_empty() {
23 serializer.serialize_none()
24 } else {
25 meta.serialize(serializer)
26 }
27}
28
29fn deserialize_meta_or_null<'de, D>(deserializer: D) -> Result<MetaMapType, D::Error>
30where
31 D: serde::Deserializer<'de>,
32{
33 Option::<MetaMapType>::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
34}
35
36pub const OTYPE_CLIENT: &str = "client";
39pub const OTYPE_WINDOW: &str = "window";
40pub const OTYPE_WORKSPACE: &str = "workspace";
41pub const OTYPE_TAB: &str = "tab";
42pub const OTYPE_LAYOUT: &str = "layout";
43pub const OTYPE_BLOCK: &str = "block";
44pub const OTYPE_TEMP: &str = "temp";
45
46pub const VALID_OTYPES: &[&str] = &[
47 OTYPE_CLIENT,
48 OTYPE_WINDOW,
49 OTYPE_WORKSPACE,
50 OTYPE_TAB,
51 OTYPE_LAYOUT,
52 OTYPE_BLOCK,
53 OTYPE_TEMP,
54];
55
56pub type MetaMapType = HashMap<String, serde_json::Value>;
60
61pub fn merge_meta(base: &MetaMapType, update: &MetaMapType, merge_special: bool) -> MetaMapType {
66 let mut result = base.clone();
67
68 for (k, v) in update {
70 if !k.ends_with(":*") {
71 continue;
72 }
73 let is_true = matches!(v, serde_json::Value::Bool(true));
75 if !is_true {
76 continue;
77 }
78 let prefix = k.trim_end_matches(":*");
79 if prefix.is_empty() {
80 continue;
81 }
82 let prefix_colon = format!("{prefix}:");
83 result.retain(|k2, _| k2 != prefix && !k2.starts_with(&prefix_colon));
84 }
85
86 for (k, v) in update {
88 if !merge_special && k.starts_with("display:") {
89 continue;
90 }
91 if k.ends_with(":*") {
92 continue;
93 }
94 if v.is_null() {
95 result.remove(k);
96 continue;
97 }
98 result.insert(k.clone(), v.clone());
99 }
100
101 result
102}
103
104pub fn meta_get_string(meta: &MetaMapType, key: &str, default: &str) -> String {
106 meta.get(key)
107 .and_then(|v| v.as_str())
108 .unwrap_or(default)
109 .to_string()
110}
111
112pub fn meta_get_bool(meta: &MetaMapType, key: &str, default: bool) -> bool {
114 meta.get(key).and_then(|v| v.as_bool()).unwrap_or(default)
115}
116
117pub trait WaveObj: Serialize + for<'de> Deserialize<'de> {
122 fn get_otype() -> &'static str;
123 fn get_oid(&self) -> &str;
124 #[allow(dead_code)]
125 fn set_oid(&mut self, oid: String);
126 #[allow(dead_code)]
127 fn get_version(&self) -> i64;
128 fn set_version(&mut self, version: i64);
129 #[allow(dead_code)]
130 fn get_meta(&self) -> &MetaMapType;
131 #[allow(dead_code)]
132 fn set_meta(&mut self, meta: MetaMapType);
133
134 #[allow(dead_code)]
135 fn oref(&self) -> ORef {
136 ORef::new(Self::get_otype(), self.get_oid())
137 }
138}
139
140macro_rules! impl_wave_obj {
143 ($ty:ty, $otype:expr) => {
144 impl WaveObj for $ty {
145 fn get_otype() -> &'static str {
146 $otype
147 }
148 fn get_oid(&self) -> &str {
149 &self.oid
150 }
151 fn set_oid(&mut self, oid: String) {
152 self.oid = oid;
153 }
154 fn get_version(&self) -> i64 {
155 self.version
156 }
157 fn set_version(&mut self, version: i64) {
158 self.version = version;
159 }
160 fn get_meta(&self) -> &MetaMapType {
161 &self.meta
162 }
163 fn set_meta(&mut self, meta: MetaMapType) {
164 self.meta = meta;
165 }
166 }
167 };
168}
169
170pub const UPDATE_TYPE_UPDATE: &str = "update";
173#[allow(dead_code)]
174pub const UPDATE_TYPE_DELETE: &str = "delete";
175
176#[allow(dead_code)]
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
180pub struct UIContext {
181 #[serde(rename = "windowid")]
182 pub window_id: String,
183 #[serde(rename = "activetabid")]
184 pub active_tab_id: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, Default)]
190pub struct Point {
191 pub x: i64,
192 pub y: i64,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, Default)]
196pub struct WinSize {
197 pub width: i64,
198 pub height: i64,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, Default)]
202pub struct TermSize {
203 pub rows: i64,
204 pub cols: i64,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
210pub struct RuntimeOpts {
211 #[serde(default, skip_serializing_if = "is_default_term_size")]
212 pub termsize: TermSize,
213 #[serde(default, skip_serializing_if = "is_default_win_size")]
214 pub winsize: WinSize,
215}
216
217fn is_default_term_size(ts: &TermSize) -> bool {
218 ts.rows == 0 && ts.cols == 0
219}
220fn is_default_win_size(ws: &WinSize) -> bool {
221 ws.width == 0 && ws.height == 0
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, Default)]
227pub struct FileDef {
228 #[serde(default, skip_serializing_if = "String::is_empty")]
229 pub content: String,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub meta: Option<HashMap<String, serde_json::Value>>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, Default)]
235pub struct BlockDef {
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub files: Option<HashMap<String, FileDef>>,
238 #[serde(default, skip_serializing_if = "MetaMapType::is_empty")]
239 pub meta: MetaMapType,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct StickerClickOpts {
246 #[serde(default, skip_serializing_if = "String::is_empty")]
247 pub sendinput: String,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub createblock: Option<BlockDef>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct StickerDisplayOpts {
254 #[serde(default)]
255 pub icon: String,
256 #[serde(default)]
257 pub imgsrc: String,
258 #[serde(default, skip_serializing_if = "String::is_empty")]
259 pub svgblob: String,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct StickerType {
264 pub stickertype: String,
265 pub style: HashMap<String, serde_json::Value>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub clickopts: Option<StickerClickOpts>,
268 pub display: StickerDisplayOpts,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct LayoutActionData {
275 pub actiontype: String,
276 pub actionid: String,
277 pub blockid: String,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub nodesize: Option<u32>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub indexarr: Option<Vec<i32>>,
282 #[serde(default)]
283 pub focused: bool,
284 #[serde(default)]
285 pub magnified: bool,
286 #[serde(default)]
287 pub ephemeral: bool,
288 #[serde(default, skip_serializing_if = "String::is_empty")]
289 pub targetblockid: String,
290 #[serde(default, skip_serializing_if = "String::is_empty")]
291 pub position: String,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct LeafOrderEntry {
296 pub nodeid: String,
297 pub blockid: String,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, Default)]
306pub struct Client {
307 pub oid: String,
308 pub version: i64,
309 #[serde(default)]
310 pub windowids: Vec<String>,
311 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
312 pub meta: MetaMapType,
313 #[serde(default, skip_serializing_if = "is_zero_i64")]
314 pub tosagreed: i64,
315 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
316 pub hasoldhistory: bool,
317 #[serde(default, skip_serializing_if = "String::is_empty")]
318 pub tempoid: String,
319}
320
321impl_wave_obj!(Client, OTYPE_CLIENT);
322
323#[derive(Debug, Clone, Serialize, Deserialize, Default)]
325pub struct Window {
326 pub oid: String,
327 pub version: i64,
328 #[serde(default)]
329 pub workspaceid: String,
330 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
331 pub isnew: bool,
332 #[serde(default)]
333 pub pos: Point,
334 #[serde(default)]
335 pub winsize: WinSize,
336 #[serde(default)]
337 pub lastfocusts: i64,
338 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
339 pub meta: MetaMapType,
340}
341
342impl_wave_obj!(Window, OTYPE_WINDOW);
343
344#[derive(Debug, Clone, Serialize, Deserialize, Default)]
346pub struct Workspace {
347 pub oid: String,
348 pub version: i64,
349 #[serde(default, skip_serializing_if = "String::is_empty")]
350 pub name: String,
351 #[serde(default)]
352 pub tabids: Vec<String>,
353 #[serde(default)]
354 pub pinnedtabids: Vec<String>,
355 #[serde(default)]
356 pub activetabid: String,
357 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
358 pub meta: MetaMapType,
359}
360
361impl_wave_obj!(Workspace, OTYPE_WORKSPACE);
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct WorkspaceListEntry {
367 pub workspaceid: String,
368 #[serde(default)]
369 pub windowid: String,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, Default)]
374pub struct Tab {
375 pub oid: String,
376 pub version: i64,
377 #[serde(default)]
378 pub name: String,
379 #[serde(default)]
380 pub layoutstate: String,
381 #[serde(default)]
382 pub blockids: Vec<String>,
383 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
384 pub meta: MetaMapType,
385}
386
387impl_wave_obj!(Tab, OTYPE_TAB);
388
389pub use agentmux_common::{FlexDirection, LayoutNode, LayoutNodeData};
396
397#[derive(Debug, Clone, Serialize, Deserialize, Default)]
399pub struct LayoutState {
400 pub oid: String,
401 pub version: i64,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub rootnode: Option<LayoutNode>,
406 #[serde(default, skip_serializing_if = "String::is_empty")]
407 pub magnifiednodeid: String,
408 #[serde(default, skip_serializing_if = "String::is_empty")]
409 pub focusednodeid: String,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub leaforder: Option<Vec<LeafOrderEntry>>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub pendingbackendactions: Option<Vec<LayoutActionData>>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub meta: Option<MetaMapType>,
416}
417
418impl WaveObj for LayoutState {
420 fn get_otype() -> &'static str {
421 OTYPE_LAYOUT
422 }
423 fn get_oid(&self) -> &str {
424 &self.oid
425 }
426 fn set_oid(&mut self, oid: String) {
427 self.oid = oid;
428 }
429 fn get_version(&self) -> i64 {
430 self.version
431 }
432 fn set_version(&mut self, version: i64) {
433 self.version = version;
434 }
435 fn get_meta(&self) -> &MetaMapType {
436 static EMPTY: std::sync::LazyLock<MetaMapType> = std::sync::LazyLock::new(MetaMapType::new);
437 self.meta.as_ref().unwrap_or(&EMPTY)
438 }
439 fn set_meta(&mut self, meta: MetaMapType) {
440 self.meta = Some(meta);
441 }
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, Default)]
446pub struct Block {
447 pub oid: String,
448 #[serde(default, skip_serializing_if = "String::is_empty")]
449 pub parentoref: String,
450 pub version: i64,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub runtimeopts: Option<RuntimeOpts>,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub stickers: Option<Vec<StickerType>>,
455 #[serde(default, serialize_with = "serialize_meta_as_null_if_empty", deserialize_with = "deserialize_meta_or_null")]
456 pub meta: MetaMapType,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub subblockids: Option<Vec<String>>,
459}
460
461impl_wave_obj!(Block, OTYPE_BLOCK);
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct WaveObjUpdate {
469 pub updatetype: String,
470 pub otype: String,
471 pub oid: String,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub obj: Option<serde_json::Value>,
474}
475
476fn is_zero_i64(v: &i64) -> bool {
479 *v == 0
480}
481
482pub fn wave_obj_to_json<T: WaveObj>(obj: &T) -> Result<Vec<u8>, serde_json::Error> {
485 let mut map = serde_json::to_value(obj)?;
486 if let Some(m) = map.as_object_mut() {
487 m.insert("otype".to_string(), serde_json::Value::String(T::get_otype().to_string()));
488 }
489 serde_json::to_vec(&map)
490}
491
492pub fn wave_obj_to_value<T: WaveObj>(obj: &T) -> serde_json::Value {
495 let mut map = serde_json::to_value(obj).unwrap_or_default();
496 if let Some(m) = map.as_object_mut() {
497 m.insert("otype".to_string(), serde_json::Value::String(T::get_otype().to_string()));
498 }
499 map
500}
501
502pub fn wave_obj_from_json<T: WaveObj>(data: &[u8]) -> Result<T, serde_json::Error> {
505 serde_json::from_slice(data)
506}
507
508#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_client_roundtrip() {
518 let client = Client {
519 oid: "550e8400-e29b-41d4-a716-446655440000".to_string(),
520 version: 3,
521 windowids: vec!["w1".to_string(), "w2".to_string()],
522 meta: MetaMapType::new(),
523 tosagreed: 1700000000000,
524 ..Default::default()
525 };
526 let json = wave_obj_to_json(&client).unwrap();
527 let parsed: Client = wave_obj_from_json(&json).unwrap();
528 assert_eq!(parsed.oid, client.oid);
529 assert_eq!(parsed.version, client.version);
530 assert_eq!(parsed.windowids, client.windowids);
531 assert_eq!(parsed.tosagreed, client.tosagreed);
532 }
533
534 #[test]
535 fn test_window_roundtrip() {
536 let window = Window {
537 oid: "550e8400-e29b-41d4-a716-446655440001".to_string(),
538 version: 1,
539 workspaceid: "ws-123".to_string(),
540 pos: Point { x: 100, y: 200 },
541 winsize: WinSize {
542 width: 1920,
543 height: 1080,
544 },
545 lastfocusts: 1700000000000,
546 meta: MetaMapType::new(),
547 ..Default::default()
548 };
549 let json = wave_obj_to_json(&window).unwrap();
550 let parsed: Window = wave_obj_from_json(&json).unwrap();
551 assert_eq!(parsed.workspaceid, "ws-123");
552 assert_eq!(parsed.pos.x, 100);
553 assert_eq!(parsed.winsize.width, 1920);
554 }
555
556 #[test]
557 fn test_workspace_roundtrip() {
558 let ws = Workspace {
559 oid: "ws-oid".to_string(),
560 version: 2,
561 name: "My Workspace".to_string(),
562 tabids: vec!["t1".to_string(), "t2".to_string()],
563 pinnedtabids: vec!["t0".to_string()],
564 activetabid: "t1".to_string(),
565 meta: MetaMapType::new(),
566 ..Default::default()
567 };
568 let json = wave_obj_to_json(&ws).unwrap();
569 let parsed: Workspace = wave_obj_from_json(&json).unwrap();
570 assert_eq!(parsed.name, "My Workspace");
571 assert_eq!(parsed.tabids.len(), 2);
572 assert_eq!(parsed.pinnedtabids, vec!["t0"]);
573 }
574
575 #[test]
576 fn test_tab_roundtrip() {
577 let tab = Tab {
578 oid: "tab-oid".to_string(),
579 version: 1,
580 name: "Tab 1".to_string(),
581 layoutstate: "ls-123".to_string(),
582 blockids: vec!["b1".to_string(), "b2".to_string()],
583 meta: MetaMapType::new(),
584 };
585 let json = wave_obj_to_json(&tab).unwrap();
586 let parsed: Tab = wave_obj_from_json(&json).unwrap();
587 assert_eq!(parsed.name, "Tab 1");
588 assert_eq!(parsed.blockids.len(), 2);
589 }
590
591 #[test]
592 fn test_layout_state_roundtrip() {
593 let ls = LayoutState {
595 oid: "ls-oid".to_string(),
596 version: 1,
597 rootnode: Some(LayoutNode {
598 id: "node-1".into(),
599 flex_direction: FlexDirection::Row,
600 size: 1.0,
601 children: Vec::new(),
602 data: None,
603 ..Default::default()
604 }),
605 magnifiednodeid: "node-1".to_string(),
606 ..Default::default()
607 };
608 let json = wave_obj_to_json(&ls).unwrap();
609 let parsed: LayoutState = wave_obj_from_json(&json).unwrap();
610 assert_eq!(parsed.magnifiednodeid, "node-1");
611 assert!(parsed.rootnode.is_some());
612 assert_eq!(parsed.rootnode.as_ref().unwrap().id, "node-1");
613 }
614
615 #[test]
623 fn test_layout_node_serde_compat_with_frontend_shapes() {
624 let leaf_json = serde_json::json!({
626 "id": "node-uuid-1",
627 "data": { "blockId": "block-uuid-1" },
628 "flexDirection": "row",
629 "size": 1
630 });
631 let leaf: LayoutNode = serde_json::from_value(leaf_json).unwrap();
632 assert_eq!(leaf.id, "node-uuid-1");
633 assert_eq!(leaf.flex_direction, FlexDirection::Row);
634 assert_eq!(leaf.size, 1.0);
635 assert!(leaf.children.is_empty());
636 assert_eq!(leaf.data.as_ref().unwrap().block_id, "block-uuid-1");
637 let reserialized = serde_json::to_value(&leaf).unwrap();
639 let reparsed: LayoutNode = serde_json::from_value(reserialized).unwrap();
640 assert_eq!(leaf, reparsed);
641
642 let three_pane_json = serde_json::json!({
644 "id": "root-id",
645 "flexDirection": "row",
646 "size": 10,
647 "children": [
648 {
649 "id": "agent-id",
650 "flexDirection": "column",
651 "size": 5,
652 "data": { "blockId": "agent-block" }
653 },
654 {
655 "id": "right-col-id",
656 "flexDirection": "column",
657 "size": 5,
658 "children": [
659 {
660 "id": "sysinfo-id",
661 "flexDirection": "row",
662 "size": 2,
663 "data": { "blockId": "sysinfo-block" }
664 },
665 {
666 "id": "swarm-id",
667 "flexDirection": "row",
668 "size": 8,
669 "data": { "blockId": "swarm-block" }
670 }
671 ]
672 }
673 ]
674 });
675 let root: LayoutNode = serde_json::from_value(three_pane_json).unwrap();
676 assert_eq!(root.id, "root-id");
677 assert_eq!(root.children.len(), 2);
678 assert_eq!(root.children[0].id, "agent-id");
679 assert_eq!(root.children[0].flex_direction, FlexDirection::Column);
680 assert_eq!(root.children[0].data.as_ref().unwrap().block_id, "agent-block");
681 let right_col = &root.children[1];
682 assert_eq!(right_col.children.len(), 2);
683 assert_eq!(right_col.data, None); assert_eq!(right_col.children[1].data.as_ref().unwrap().block_id, "swarm-block");
685 let reserialized = serde_json::to_value(&root).unwrap();
686 let reparsed: LayoutNode = serde_json::from_value(reserialized).unwrap();
687 assert_eq!(root, reparsed);
688
689 let leaf_no_children = serde_json::json!({
692 "id": "loner",
693 "flexDirection": "column",
694 "size": 5,
695 "data": { "blockId": "b" }
696 });
697 let parsed: LayoutNode = serde_json::from_value(leaf_no_children).unwrap();
698 assert!(parsed.children.is_empty());
699 }
700
701 #[test]
704 fn test_layout_node_defaults_flex_direction_to_row() {
705 let no_dir = serde_json::json!({"id": "x", "size": 1});
706 let parsed: LayoutNode = serde_json::from_value(no_dir).unwrap();
707 assert_eq!(parsed.flex_direction, FlexDirection::Row);
708 }
709
710 #[test]
715 fn test_layout_node_size_serializes_as_integer_when_whole() {
716 let whole = LayoutNode {
717 id: "x".into(),
718 flex_direction: FlexDirection::Row,
719 size: 10.0,
720 children: Vec::new(),
721 data: None,
722 ..Default::default()
723 };
724 let json = serde_json::to_string(&whole).unwrap();
725 assert!(json.contains("\"size\":10"), "whole sizes should serialize as integer; got {}", json);
726 assert!(!json.contains("\"size\":10.0"), "whole sizes should NOT include the decimal; got {}", json);
727
728 let frac = LayoutNode {
730 id: "y".into(),
731 flex_direction: FlexDirection::Row,
732 size: 5.5,
733 children: Vec::new(),
734 data: None,
735 ..Default::default()
736 };
737 let json = serde_json::to_string(&frac).unwrap();
738 assert!(json.contains("\"size\":5.5"), "fractional sizes preserve the decimal; got {}", json);
739
740 let reparsed: LayoutNode = serde_json::from_str(&serde_json::to_string(&whole).unwrap()).unwrap();
742 assert_eq!(reparsed.size, 10.0);
743 }
744
745 #[test]
748 fn test_layout_node_data_skipped_when_none() {
749 let group = LayoutNode {
750 id: "g".into(),
751 flex_direction: FlexDirection::Row,
752 size: 1.0,
753 children: Vec::new(),
754 data: None,
755 ..Default::default()
756 };
757 let json = serde_json::to_value(&group).unwrap();
758 assert!(json.get("data").is_none(), "data: None must skip-serialize");
759 }
760
761 #[test]
767 fn test_layout_node_preserves_unknown_fields() {
768 let json_with_extras = serde_json::json!({
769 "id": "n",
770 "flexDirection": "row",
771 "size": 1,
772 "backendLayoutVersion": 7,
773 "data": {
774 "blockId": "b",
775 "renderHint": "compact"
776 }
777 });
778 let node: LayoutNode = serde_json::from_value(json_with_extras).unwrap();
779 assert_eq!(node.extra.get("backendLayoutVersion"), Some(&serde_json::json!(7)));
780 assert_eq!(
781 node.data.as_ref().unwrap().extra.get("renderHint"),
782 Some(&serde_json::json!("compact"))
783 );
784 let reserialized = serde_json::to_value(&node).unwrap();
786 assert_eq!(reserialized.get("backendLayoutVersion"), Some(&serde_json::json!(7)));
787 assert_eq!(
788 reserialized.get("data").and_then(|d| d.get("renderHint")),
789 Some(&serde_json::json!("compact"))
790 );
791 let no_extras = LayoutNode {
793 id: "x".into(),
794 flex_direction: FlexDirection::Row,
795 size: 1.0,
796 ..Default::default()
797 };
798 let json = serde_json::to_value(&no_extras).unwrap();
799 assert!(json.get("extra").is_none());
800 }
801
802 #[test]
806 fn test_layout_node_data_missing_block_id_fails_deserialization() {
807 let missing_id = serde_json::json!({ "renderHint": "compact" });
808 let result = serde_json::from_value::<LayoutNodeData>(missing_id);
809 assert!(result.is_err(), "missing blockId must fail deserialization");
810 }
811
812 #[test]
813 fn test_block_roundtrip() {
814 let block = Block {
815 oid: "blk-oid".to_string(),
816 version: 5,
817 parentoref: "tab:parent-id".to_string(),
818 meta: {
819 let mut m = MetaMapType::new();
820 m.insert("view".to_string(), serde_json::json!("term"));
821 m
822 },
823 ..Default::default()
824 };
825 let json = wave_obj_to_json(&block).unwrap();
826 let parsed: Block = wave_obj_from_json(&json).unwrap();
827 assert_eq!(parsed.parentoref, "tab:parent-id");
828 assert_eq!(
829 parsed.meta.get("view").and_then(|v| v.as_str()),
830 Some("term")
831 );
832 }
833
834 #[test]
835 fn test_wire_compat_go_json_client() {
836 let go_json = r#"{"oid":"abc-123","otype":"client","version":2,"windowids":["w1"],"meta":{"view":"term"},"tosagreed":1700000000000}"#;
838 let client: Client = serde_json::from_str(go_json).unwrap();
839 assert_eq!(client.oid, "abc-123");
840 assert_eq!(client.version, 2);
841 assert_eq!(client.windowids, vec!["w1"]);
842 assert_eq!(client.tosagreed, 1700000000000);
843 }
844
845 #[test]
846 fn test_wire_compat_go_json_block() {
847 let go_json = r#"{"oid":"blk-1","otype":"block","version":3,"parentoref":"tab:t1","meta":{"view":"term","cmd":"ls"}}"#;
848 let block: Block = serde_json::from_str(go_json).unwrap();
849 assert_eq!(block.oid, "blk-1");
850 assert_eq!(block.parentoref, "tab:t1");
851 assert_eq!(block.version, 3);
852 }
853
854 #[test]
855 fn test_wire_compat_go_json_tab() {
856 let go_json = r#"{"oid":"tab-1","otype":"tab","version":1,"name":"Shell","layoutstate":"ls-1","blockids":["b1","b2"],"meta":{}}"#;
857 let tab: Tab = serde_json::from_str(go_json).unwrap();
858 assert_eq!(tab.name, "Shell");
859 assert_eq!(tab.blockids, vec!["b1", "b2"]);
860 }
861
862 #[test]
863 fn test_wave_obj_to_json_includes_otype() {
864 let client = Client {
865 oid: "test".to_string(),
866 version: 1,
867 ..Default::default()
868 };
869 let json_bytes = wave_obj_to_json(&client).unwrap();
870 let v: serde_json::Value = serde_json::from_slice(&json_bytes).unwrap();
871 assert_eq!(v["otype"], "client");
872 }
873
874 #[test]
875 fn test_merge_meta_basic() {
876 let mut base = MetaMapType::new();
877 base.insert("view".into(), serde_json::json!("term"));
878 base.insert("cmd".into(), serde_json::json!("ls"));
879
880 let mut update = MetaMapType::new();
881 update.insert("cmd".into(), serde_json::json!("pwd"));
882 update.insert("icon".into(), serde_json::json!("star"));
883
884 let result = merge_meta(&base, &update, true);
885 assert_eq!(result["view"], "term");
886 assert_eq!(result["cmd"], "pwd");
887 assert_eq!(result["icon"], "star");
888 }
889
890 #[test]
891 fn test_merge_meta_null_deletes() {
892 let mut base = MetaMapType::new();
893 base.insert("view".into(), serde_json::json!("term"));
894 base.insert("cmd".into(), serde_json::json!("ls"));
895
896 let mut update = MetaMapType::new();
897 update.insert("cmd".into(), serde_json::Value::Null);
898
899 let result = merge_meta(&base, &update, true);
900 assert_eq!(result.get("view").unwrap(), "term");
901 assert!(!result.contains_key("cmd"));
902 }
903
904 #[test]
905 fn test_merge_meta_section_clear() {
906 let mut base = MetaMapType::new();
907 base.insert("frame".into(), serde_json::json!(true));
908 base.insert("frame:title".into(), serde_json::json!("hello"));
909 base.insert("frame:icon".into(), serde_json::json!("star"));
910 base.insert("cmd".into(), serde_json::json!("ls"));
911
912 let mut update = MetaMapType::new();
913 update.insert("frame:*".into(), serde_json::json!(true));
914
915 let result = merge_meta(&base, &update, true);
916 assert!(!result.contains_key("frame"));
917 assert!(!result.contains_key("frame:title"));
918 assert!(!result.contains_key("frame:icon"));
919 assert_eq!(result["cmd"], "ls"); }
921
922 #[test]
923 fn test_merge_meta_skip_display_when_not_special() {
924 let base = MetaMapType::new();
925 let mut update = MetaMapType::new();
926 update.insert("display:name".into(), serde_json::json!("test"));
927 update.insert("view".into(), serde_json::json!("term"));
928
929 let result = merge_meta(&base, &update, false);
930 assert!(!result.contains_key("display:name"));
931 assert_eq!(result["view"], "term");
932 }
933
934 #[test]
935 fn test_meta_get_string() {
936 let mut meta = MetaMapType::new();
937 meta.insert("view".into(), serde_json::json!("term"));
938 assert_eq!(meta_get_string(&meta, "view", "default"), "term");
939 assert_eq!(meta_get_string(&meta, "missing", "default"), "default");
940 }
941
942 #[test]
943 fn test_meta_get_bool() {
944 let mut meta = MetaMapType::new();
945 meta.insert("edit".into(), serde_json::json!(true));
946 assert!(meta_get_bool(&meta, "edit", false));
947 assert!(!meta_get_bool(&meta, "missing", false));
948 }
949}