Skip to main content

aimux_protocol/
messages.rs

1use serde::{Deserialize, Serialize};
2
3use crate::types::*;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub enum Request {
7    // Session commands
8    NewSession { name: String, shell: Option<String>, #[serde(default)] layout: Option<String>, #[serde(default)] pane_alias: Option<String>, #[serde(default)] template: Option<String> },
9    KillSession { name: String },
10    ListSessions,
11    HasSession { name: String },
12
13    // Window commands
14    NewWindow { session: SessionId, shell: Option<String>, #[serde(default)] layout: Option<String>, #[serde(default)] pane_alias: Option<String> },
15    KillWindow { session: SessionId, window: WindowId },
16    ListWindows { session: SessionId },
17    SelectWindow { session: SessionId, window: WindowId },
18
19    // Pane commands
20    SplitPane { target: String, horizontal: bool, shell: Option<String>, #[serde(default)] pane_alias: Option<String> },
21    KillPane { pane_id: PaneId },
22    ListPanes { session: SessionId },
23    SelectPane { pane_id: PaneId },
24    PaneInfo { pane_id: PaneId },
25
26    // Resize
27    ResizePane { pane_id: PaneId, cols: u16, rows: u16 },
28
29    // Core I/O
30    SendKeys { pane_id: PaneId, keys: String },
31    Capture { pane_id: PaneId, json: bool, scrollback: Option<ScrollbackRange>, #[serde(default)] filter: Option<CaptureFilter> },
32    Wait { pane_id: PaneId, pattern: String, timeout_secs: Option<u64>, #[serde(default)] return_match: bool, #[serde(default)] capture_on_match: bool, #[serde(default)] scrollback: bool },
33    Exec { pane_id: PaneId, command: String, wait: bool, capture: bool, scrollback: Option<ScrollbackRange>, #[serde(default)] filter: Option<CaptureFilter> },
34    Subscribe { pane_id: PaneId, events: Vec<String> },
35
36    // Options
37    SetOption { target: String, key: String, value: String },
38
39    // Pane marks
40    MarkPane { pane_id: PaneId, mark: String },
41    UnmarkPane { mark: String },
42    ListMarks,
43
44    // Pane swap
45    SwapPanes { pane_a: PaneId, pane_b: PaneId },
46
47    // Pane move
48    MovePane { pane_id: PaneId, target_session: SessionId, target_window: WindowId },
49
50    // Layout
51    ApplyLayout { session: SessionId, window: WindowId, layout_name: String },
52    SelectLayout { session: SessionId, window: WindowId, layout: String },
53    SaveLayout { session: SessionId, window: WindowId, name: String },
54    LoadLayout { session: SessionId, window: WindowId, name: String },
55    ListLayouts,
56
57    // Server
58    Ping,
59    KillServer,
60    ServerStatus,
61
62    // Visual client
63    Attach { session: SessionId },
64
65    // Config
66    ReloadConfig,
67
68    // Batch
69    Batch { requests: Vec<Request> },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum Response {
74    // Session responses
75    SessionCreated { session_id: SessionId, pane_id: PaneId },
76    SessionKilled,
77    Sessions(Vec<SessionInfo>),
78    SessionExists(bool),
79
80    // Window responses
81    WindowCreated { window_id: WindowId, pane_id: PaneId },
82    WindowKilled,
83    Windows(Vec<WindowInfo>),
84    WindowSelected,
85
86    // Pane responses
87    PaneSplit { pane_id: PaneId },
88    PaneKilled,
89    Panes(Vec<PaneInfo>),
90    PaneSelected,
91    PaneDetail(PaneInfo),
92
93    // Resize
94    PaneResized,
95
96    // Core I/O responses
97    KeysSent,
98    CaptureResult(CaptureResult),
99    WaitMatched {
100        #[serde(default, skip_serializing_if = "Option::is_none")]
101        matched: Option<String>,
102        #[serde(default, skip_serializing_if = "Option::is_none")]
103        line: Option<String>,
104        #[serde(default, skip_serializing_if = "Option::is_none")]
105        capture: Option<CaptureResult>,
106    },
107    ExecResult { output: Option<CaptureResult> },
108    Subscribed { stream_pipe: String },
109
110    // Options
111    OptionSet,
112
113    // Server
114    Pong,
115    ServerKilling,
116    ServerStatus(ServerStatusInfo),
117
118    // Pane marks
119    MarkSet,
120    MarkRemoved,
121    Marks(Vec<MarkInfo>),
122
123    // Pane swap
124    PanesSwapped,
125
126    // Pane move
127    PaneMoved,
128
129    // Layout
130    LayoutApplied { pane_ids: Vec<PaneId> },
131    LayoutSelected,
132    LayoutSaved,
133    LayoutLoaded { pane_ids: Vec<PaneId> },
134    Layouts(Vec<LayoutListEntry>),
135
136    // Batch
137    BatchResult { responses: Vec<Response> },
138
139    // Generic
140    Ok,
141    Error { message: String },
142
143    // Visual client
144    AttachAccepted { layout: LayoutInfo },
145
146    // Config
147    ConfigReloaded,
148}
149
150/// Messages pushed from server to an attached client.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum ServerPush {
153    ScreenSnapshot {
154        pane_id: PaneId,
155        cells: Vec<Vec<ScreenCell>>,
156        cursor: CursorPos,
157        size: (u16, u16),
158        cursor_visible: bool,
159        title: String,
160    },
161    LayoutChanged(LayoutInfo),
162    Error { message: String },
163    ServerShutdown,
164}
165
166/// Messages sent from attached client to server.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub enum ClientInput {
169    Keys { data: Vec<u8> },
170    Resize { cols: u16, rows: u16 },
171    Detach,
172    Command(Request),
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::types::{CaptureFilter, CaptureResult, CursorPos, PaneInfo, ProcessInfo, SessionInfo, WindowInfo};
179
180    /// Helper: serialize to MessagePack and deserialize back.
181    fn roundtrip_request(req: &Request) -> Request {
182        let bytes = rmp_serde::to_vec(req).unwrap();
183        rmp_serde::from_slice(&bytes).unwrap()
184    }
185
186    fn roundtrip_response(resp: &Response) -> Response {
187        let bytes = rmp_serde::to_vec(resp).unwrap();
188        rmp_serde::from_slice(&bytes).unwrap()
189    }
190
191    #[test]
192    fn request_new_session_roundtrip() {
193        let req = Request::NewSession {
194            name: "test".into(),
195            shell: None,
196            layout: None,
197            pane_alias: None,
198            template: None,
199        };
200        let decoded = roundtrip_request(&req);
201        match decoded {
202            Request::NewSession { name, shell, layout, pane_alias, template } => {
203                assert_eq!(name, "test");
204                assert!(shell.is_none());
205                assert!(layout.is_none());
206                assert!(pane_alias.is_none());
207                assert!(template.is_none());
208            }
209            _ => panic!("wrong variant"),
210        }
211    }
212
213    #[test]
214    fn request_send_keys_roundtrip() {
215        let req = Request::SendKeys {
216            pane_id: "%0".into(),
217            keys: "ls\r".into(),
218        };
219        let decoded = roundtrip_request(&req);
220        match decoded {
221            Request::SendKeys { pane_id, keys } => {
222                assert_eq!(pane_id, "%0");
223                assert_eq!(keys, "ls\r");
224            }
225            _ => panic!("wrong variant"),
226        }
227    }
228
229    #[test]
230    fn request_split_pane_roundtrip() {
231        let req = Request::SplitPane {
232            target: "%0".into(),
233            horizontal: true,
234            shell: Some("cmd.exe".into()),
235            pane_alias: None,
236        };
237        let decoded = roundtrip_request(&req);
238        match decoded {
239            Request::SplitPane {
240                target,
241                horizontal,
242                shell,
243                pane_alias,
244            } => {
245                assert_eq!(target, "%0");
246                assert!(horizontal);
247                assert_eq!(shell.unwrap(), "cmd.exe");
248                assert!(pane_alias.is_none());
249            }
250            _ => panic!("wrong variant"),
251        }
252    }
253
254    #[test]
255    fn request_wait_roundtrip() {
256        let req = Request::Wait {
257            pane_id: "%1".into(),
258            pattern: ">".into(),
259            timeout_secs: Some(30),
260            return_match: false,
261            capture_on_match: false,
262            scrollback: false,
263        };
264        let decoded = roundtrip_request(&req);
265        match decoded {
266            Request::Wait {
267                pane_id,
268                pattern,
269                timeout_secs,
270                return_match,
271                capture_on_match,
272                scrollback,
273            } => {
274                assert_eq!(pane_id, "%1");
275                assert_eq!(pattern, ">");
276                assert_eq!(timeout_secs, Some(30));
277                assert!(!return_match);
278                assert!(!capture_on_match);
279                assert!(!scrollback);
280            }
281            _ => panic!("wrong variant"),
282        }
283    }
284
285    #[test]
286    fn request_exec_roundtrip() {
287        let req = Request::Exec {
288            pane_id: "%0".into(),
289            command: "dir".into(),
290            wait: true,
291            capture: true,
292            scrollback: None,
293            filter: None,
294        };
295        let decoded = roundtrip_request(&req);
296        match decoded {
297            Request::Exec {
298                pane_id,
299                command,
300                wait,
301                capture,
302                scrollback,
303                filter,
304            } => {
305                assert_eq!(pane_id, "%0");
306                assert_eq!(command, "dir");
307                assert!(wait);
308                assert!(capture);
309                assert!(scrollback.is_none());
310                assert!(filter.is_none());
311            }
312            _ => panic!("wrong variant"),
313        }
314    }
315
316    #[test]
317    fn request_new_window_roundtrip() {
318        let req = Request::NewWindow {
319            session: "work".into(),
320            shell: Some("cmd.exe".into()),
321            layout: None,
322            pane_alias: None,
323        };
324        let decoded = roundtrip_request(&req);
325        match decoded {
326            Request::NewWindow { session, shell, pane_alias, .. } => {
327                assert_eq!(session, "work");
328                assert_eq!(shell.unwrap(), "cmd.exe");
329                assert!(pane_alias.is_none());
330            }
331            _ => panic!("wrong variant"),
332        }
333
334        // Also test with None shell
335        let req2 = Request::NewWindow {
336            session: "test".into(),
337            shell: None,
338            layout: None,
339            pane_alias: None,
340        };
341        let decoded2 = roundtrip_request(&req2);
342        match decoded2 {
343            Request::NewWindow { session, shell, .. } => {
344                assert_eq!(session, "test");
345                assert!(shell.is_none());
346            }
347            _ => panic!("wrong variant"),
348        }
349    }
350
351    #[test]
352    fn request_parameterless_variants_roundtrip() {
353        for req in [Request::ListSessions, Request::Ping, Request::KillServer, Request::ReloadConfig, Request::ListMarks, Request::ServerStatus] {
354            let decoded = roundtrip_request(&req);
355            // Just verify it doesn't panic on roundtrip.
356            let _ = format!("{:?}", decoded);
357        }
358    }
359
360    #[test]
361    fn response_session_created_roundtrip() {
362        let resp = Response::SessionCreated {
363            session_id: "work".into(),
364            pane_id: "%0".into(),
365        };
366        let decoded = roundtrip_response(&resp);
367        match decoded {
368            Response::SessionCreated {
369                session_id,
370                pane_id,
371            } => {
372                assert_eq!(session_id, "work");
373                assert_eq!(pane_id, "%0");
374            }
375            _ => panic!("wrong variant"),
376        }
377    }
378
379    #[test]
380    fn response_sessions_roundtrip() {
381        let resp = Response::Sessions(vec![SessionInfo {
382            name: "s1".into(),
383            windows: vec![WindowInfo {
384                id: 0,
385                panes: vec![PaneInfo {
386                    id: "%0".into(),
387                    pid: 1234,
388                    running: true,
389                    exit_code: None,
390                    size: (80, 24),
391                    title: "powershell.exe".into(),
392                    marks: vec![],
393                }],
394                active_pane: "%0".into(),
395            }],
396            created_at: 1700000000,
397        }]);
398        let decoded = roundtrip_response(&resp);
399        match decoded {
400            Response::Sessions(sessions) => {
401                assert_eq!(sessions.len(), 1);
402                assert_eq!(sessions[0].name, "s1");
403                assert_eq!(sessions[0].windows[0].panes[0].pid, 1234);
404            }
405            _ => panic!("wrong variant"),
406        }
407    }
408
409    #[test]
410    fn response_capture_result_roundtrip() {
411        let resp = Response::CaptureResult(CaptureResult {
412            pane_id: "%0".into(),
413            lines: vec!["PS C:\\>".into(), "hello".into()],
414            scrollback_lines: vec![],
415            cursor: CursorPos { row: 2, col: 0 },
416            size: (80, 24),
417            process: ProcessInfo {
418                pid: 5678,
419                running: true,
420                exit_code: None,
421            },
422            title: "powershell.exe".into(),
423        });
424        let decoded = roundtrip_response(&resp);
425        match decoded {
426            Response::CaptureResult(cap) => {
427                assert_eq!(cap.pane_id, "%0");
428                assert_eq!(cap.lines.len(), 2);
429                assert_eq!(cap.cursor.row, 2);
430                assert_eq!(cap.process.pid, 5678);
431            }
432            _ => panic!("wrong variant"),
433        }
434    }
435
436    #[test]
437    fn response_error_roundtrip() {
438        let resp = Response::Error {
439            message: "something broke".into(),
440        };
441        let decoded = roundtrip_response(&resp);
442        match decoded {
443            Response::Error { message } => assert_eq!(message, "something broke"),
444            _ => panic!("wrong variant"),
445        }
446    }
447
448    #[test]
449    fn response_simple_variants_roundtrip() {
450        for resp in [
451            Response::SessionKilled,
452            Response::WindowKilled,
453            Response::WindowSelected,
454            Response::PaneKilled,
455            Response::PaneSelected,
456            Response::PaneResized,
457            Response::KeysSent,
458            Response::WaitMatched { matched: None, line: None, capture: None },
459            Response::OptionSet,
460            Response::Pong,
461            Response::ServerKilling,
462            Response::Ok,
463            Response::ConfigReloaded,
464            Response::MarkSet,
465            Response::MarkRemoved,
466            Response::PanesSwapped,
467            Response::PaneMoved,
468            Response::LayoutSelected,
469        ] {
470            let decoded = roundtrip_response(&resp);
471            let _ = format!("{:?}", decoded);
472        }
473    }
474
475    #[test]
476    fn request_capture_with_scrollback_roundtrip() {
477        let req = Request::Capture {
478            pane_id: "%0".into(),
479            json: false,
480            scrollback: Some(ScrollbackRange::Last(100)),
481            filter: None,
482        };
483        let decoded = roundtrip_request(&req);
484        match decoded {
485            Request::Capture {
486                pane_id,
487                json,
488                scrollback,
489                filter,
490            } => {
491                assert_eq!(pane_id, "%0");
492                assert!(!json);
493                match scrollback {
494                    Some(ScrollbackRange::Last(n)) => assert_eq!(n, 100),
495                    _ => panic!("expected ScrollbackRange::Last(100)"),
496                }
497                assert!(filter.is_none());
498            }
499            _ => panic!("wrong variant"),
500        }
501    }
502
503    #[test]
504    fn capture_result_with_scrollback_lines_roundtrip() {
505        let resp = Response::CaptureResult(CaptureResult {
506            pane_id: "%0".into(),
507            lines: vec!["visible line".into()],
508            scrollback_lines: vec!["old line 1".into(), "old line 2".into()],
509            cursor: CursorPos { row: 1, col: 0 },
510            size: (80, 24),
511            process: ProcessInfo {
512                pid: 1234,
513                running: true,
514                exit_code: None,
515            },
516            title: "cmd.exe".into(),
517        });
518        let decoded = roundtrip_response(&resp);
519        match decoded {
520            Response::CaptureResult(cap) => {
521                assert_eq!(cap.scrollback_lines.len(), 2);
522                assert_eq!(cap.scrollback_lines[0], "old line 1");
523                assert_eq!(cap.scrollback_lines[1], "old line 2");
524                assert_eq!(cap.lines.len(), 1);
525            }
526            _ => panic!("wrong variant"),
527        }
528    }
529
530    #[test]
531    fn request_resize_pane_roundtrip() {
532        let req = Request::ResizePane {
533            pane_id: "%0".into(),
534            cols: 120,
535            rows: 40,
536        };
537        let decoded = roundtrip_request(&req);
538        match decoded {
539            Request::ResizePane { pane_id, cols, rows } => {
540                assert_eq!(pane_id, "%0");
541                assert_eq!(cols, 120);
542                assert_eq!(rows, 40);
543            }
544            _ => panic!("wrong variant"),
545        }
546    }
547
548    #[test]
549    fn response_pane_resized_roundtrip() {
550        let resp = Response::PaneResized;
551        let decoded = roundtrip_response(&resp);
552        assert!(matches!(decoded, Response::PaneResized));
553    }
554
555    #[test]
556    fn request_exec_with_scrollback_roundtrip() {
557        let req = Request::Exec {
558            pane_id: "%0".into(),
559            command: "dir".into(),
560            wait: true,
561            capture: true,
562            scrollback: Some(ScrollbackRange::All),
563            filter: None,
564        };
565        let decoded = roundtrip_request(&req);
566        match decoded {
567            Request::Exec {
568                scrollback, ..
569            } => {
570                assert!(matches!(scrollback, Some(ScrollbackRange::All)));
571            }
572            _ => panic!("wrong variant"),
573        }
574    }
575
576    #[test]
577    fn request_attach_roundtrip() {
578        let req = Request::Attach {
579            session: "work".into(),
580        };
581        let decoded = roundtrip_request(&req);
582        match decoded {
583            Request::Attach { session } => assert_eq!(session, "work"),
584            _ => panic!("wrong variant"),
585        }
586    }
587
588    #[test]
589    fn response_attach_accepted_roundtrip() {
590        let layout = LayoutInfo {
591            session: "work".into(),
592            window_id: 0,
593            panes: vec![PaneLayout {
594                pane_id: "%0".into(),
595                x: 0,
596                y: 0,
597                width: 80,
598                height: 24,
599                is_active: true,
600                is_zoomed: false,
601            }],
602            terminal_cols: 80,
603            terminal_rows: 24,
604            windows: vec![WindowBarInfo {
605                id: 0,
606                title: "bash".into(),
607                is_active: true,
608            }],
609        };
610        let resp = Response::AttachAccepted {
611            layout: layout.clone(),
612        };
613        let decoded = roundtrip_response(&resp);
614        match decoded {
615            Response::AttachAccepted { layout: l } => assert_eq!(l, layout),
616            _ => panic!("wrong variant"),
617        }
618    }
619
620    fn roundtrip_server_push(push: &ServerPush) -> ServerPush {
621        let bytes = rmp_serde::to_vec(push).unwrap();
622        rmp_serde::from_slice(&bytes).unwrap()
623    }
624
625    fn roundtrip_client_input(input: &ClientInput) -> ClientInput {
626        let bytes = rmp_serde::to_vec(input).unwrap();
627        rmp_serde::from_slice(&bytes).unwrap()
628    }
629
630    #[test]
631    fn server_push_screen_snapshot_roundtrip() {
632        let push = ServerPush::ScreenSnapshot {
633            pane_id: "%0".into(),
634            cells: vec![vec![ScreenCell {
635                ch: 'A',
636                attrs: ScreenCellAttrs {
637                    bold: true,
638                    fg: ScreenColor::Rgb(255, 0, 0),
639                    ..Default::default()
640                },
641            }]],
642            cursor: CursorPos { row: 0, col: 1 },
643            size: (80, 24),
644            cursor_visible: true,
645            title: "cmd.exe".into(),
646        };
647        let decoded = roundtrip_server_push(&push);
648        match decoded {
649            ServerPush::ScreenSnapshot {
650                pane_id,
651                cells,
652                cursor,
653                size,
654                cursor_visible,
655                title,
656            } => {
657                assert_eq!(pane_id, "%0");
658                assert_eq!(cells.len(), 1);
659                assert_eq!(cells[0][0].ch, 'A');
660                assert!(cells[0][0].attrs.bold);
661                assert_eq!(cells[0][0].attrs.fg, ScreenColor::Rgb(255, 0, 0));
662                assert_eq!(cursor.row, 0);
663                assert_eq!(cursor.col, 1);
664                assert_eq!(size, (80, 24));
665                assert!(cursor_visible);
666                assert_eq!(title, "cmd.exe");
667            }
668            _ => panic!("wrong variant"),
669        }
670    }
671
672    #[test]
673    fn server_push_layout_changed_roundtrip() {
674        let layout = LayoutInfo {
675            session: "s".into(),
676            window_id: 1,
677            panes: vec![],
678            terminal_cols: 120,
679            terminal_rows: 40,
680            windows: vec![
681                WindowBarInfo {
682                    id: 0,
683                    title: "win0".into(),
684                    is_active: false,
685                },
686                WindowBarInfo {
687                    id: 1,
688                    title: "win1".into(),
689                    is_active: true,
690                },
691            ],
692        };
693        let push = ServerPush::LayoutChanged(layout.clone());
694        let decoded = roundtrip_server_push(&push);
695        match decoded {
696            ServerPush::LayoutChanged(l) => assert_eq!(l, layout),
697            _ => panic!("wrong variant"),
698        }
699    }
700
701    #[test]
702    fn server_push_error_and_shutdown_roundtrip() {
703        let push = ServerPush::Error {
704            message: "oops".into(),
705        };
706        let decoded = roundtrip_server_push(&push);
707        match decoded {
708            ServerPush::Error { message } => assert_eq!(message, "oops"),
709            _ => panic!("wrong variant"),
710        }
711
712        let push2 = ServerPush::ServerShutdown;
713        let decoded2 = roundtrip_server_push(&push2);
714        assert!(matches!(decoded2, ServerPush::ServerShutdown));
715    }
716
717    #[test]
718    fn client_input_keys_roundtrip() {
719        let input = ClientInput::Keys {
720            data: vec![27, 91, 65], // ESC [ A (arrow up)
721        };
722        let decoded = roundtrip_client_input(&input);
723        match decoded {
724            ClientInput::Keys { data } => assert_eq!(data, vec![27, 91, 65]),
725            _ => panic!("wrong variant"),
726        }
727    }
728
729    #[test]
730    fn client_input_resize_roundtrip() {
731        let input = ClientInput::Resize {
732            cols: 120,
733            rows: 40,
734        };
735        let decoded = roundtrip_client_input(&input);
736        match decoded {
737            ClientInput::Resize { cols, rows } => {
738                assert_eq!(cols, 120);
739                assert_eq!(rows, 40);
740            }
741            _ => panic!("wrong variant"),
742        }
743    }
744
745    #[test]
746    fn client_input_detach_roundtrip() {
747        let input = ClientInput::Detach;
748        let decoded = roundtrip_client_input(&input);
749        assert!(matches!(decoded, ClientInput::Detach));
750    }
751
752    #[test]
753    fn client_input_command_roundtrip() {
754        let input = ClientInput::Command(Request::Ping);
755        let decoded = roundtrip_client_input(&input);
756        match decoded {
757            ClientInput::Command(req) => assert!(matches!(req, Request::Ping)),
758            _ => panic!("wrong variant"),
759        }
760    }
761
762    #[test]
763    fn screen_color_variants_roundtrip() {
764        let colors = vec![
765            ScreenColor::Default,
766            ScreenColor::Indexed(196),
767            ScreenColor::Rgb(0, 128, 255),
768        ];
769        for color in &colors {
770            let bytes = rmp_serde::to_vec(color).unwrap();
771            let decoded: ScreenColor = rmp_serde::from_slice(&bytes).unwrap();
772            assert_eq!(&decoded, color);
773        }
774    }
775
776    #[test]
777    fn window_bar_info_roundtrip() {
778        let info = WindowBarInfo {
779            id: 3,
780            title: "my window".into(),
781            is_active: false,
782        };
783        let bytes = rmp_serde::to_vec(&info).unwrap();
784        let decoded: WindowBarInfo = rmp_serde::from_slice(&bytes).unwrap();
785        assert_eq!(decoded, info);
786    }
787
788    #[test]
789    fn request_mark_pane_roundtrip() {
790        let req = Request::MarkPane {
791            pane_id: "%0".into(),
792            mark: "build".into(),
793        };
794        let decoded = roundtrip_request(&req);
795        match decoded {
796            Request::MarkPane { pane_id, mark } => {
797                assert_eq!(pane_id, "%0");
798                assert_eq!(mark, "build");
799            }
800            _ => panic!("wrong variant"),
801        }
802    }
803
804    #[test]
805    fn request_unmark_pane_roundtrip() {
806        let req = Request::UnmarkPane {
807            mark: "build".into(),
808        };
809        let decoded = roundtrip_request(&req);
810        match decoded {
811            Request::UnmarkPane { mark } => assert_eq!(mark, "build"),
812            _ => panic!("wrong variant"),
813        }
814    }
815
816    #[test]
817    fn response_marks_roundtrip() {
818        let resp = Response::Marks(vec![
819            MarkInfo {
820                mark: "build".into(),
821                pane_id: "%0".into(),
822            },
823            MarkInfo {
824                mark: "test".into(),
825                pane_id: "%1".into(),
826            },
827        ]);
828        let decoded = roundtrip_response(&resp);
829        match decoded {
830            Response::Marks(marks) => {
831                assert_eq!(marks.len(), 2);
832                assert_eq!(marks[0].mark, "build");
833                assert_eq!(marks[0].pane_id, "%0");
834                assert_eq!(marks[1].mark, "test");
835                assert_eq!(marks[1].pane_id, "%1");
836            }
837            _ => panic!("wrong variant"),
838        }
839    }
840
841    #[test]
842    fn request_swap_panes_roundtrip() {
843        let req = Request::SwapPanes {
844            pane_a: "%0".into(),
845            pane_b: "%1".into(),
846        };
847        let decoded = roundtrip_request(&req);
848        match decoded {
849            Request::SwapPanes { pane_a, pane_b } => {
850                assert_eq!(pane_a, "%0");
851                assert_eq!(pane_b, "%1");
852            }
853            _ => panic!("wrong variant"),
854        }
855    }
856
857    #[test]
858    fn response_panes_swapped_roundtrip() {
859        let resp = Response::PanesSwapped;
860        let decoded = roundtrip_response(&resp);
861        assert!(matches!(decoded, Response::PanesSwapped));
862    }
863
864    #[test]
865    fn request_move_pane_roundtrip() {
866        let req = Request::MovePane {
867            pane_id: "%0".into(),
868            target_session: "work".into(),
869            target_window: 1,
870        };
871        let decoded = roundtrip_request(&req);
872        match decoded {
873            Request::MovePane { pane_id, target_session, target_window } => {
874                assert_eq!(pane_id, "%0");
875                assert_eq!(target_session, "work");
876                assert_eq!(target_window, 1);
877            }
878            _ => panic!("wrong variant"),
879        }
880    }
881
882    #[test]
883    fn response_pane_moved_roundtrip() {
884        let resp = Response::PaneMoved;
885        let decoded = roundtrip_response(&resp);
886        assert!(matches!(decoded, Response::PaneMoved));
887    }
888
889    #[test]
890    fn pane_info_with_marks_roundtrip() {
891        let info = PaneInfo {
892            id: "%0".into(),
893            pid: 1234,
894            running: true,
895            exit_code: None,
896            size: (80, 24),
897            title: "powershell.exe".into(),
898            marks: vec!["build".into(), "main".into()],
899        };
900        let bytes = rmp_serde::to_vec(&info).unwrap();
901        let decoded: PaneInfo = rmp_serde::from_slice(&bytes).unwrap();
902        assert_eq!(decoded.id, "%0");
903        assert_eq!(decoded.marks, vec!["build", "main"]);
904    }
905
906    #[test]
907    fn request_apply_layout_roundtrip() {
908        let req = Request::ApplyLayout {
909            session: "work".into(),
910            window: 0,
911            layout_name: "dev".into(),
912        };
913        let decoded = roundtrip_request(&req);
914        match decoded {
915            Request::ApplyLayout { session, window, layout_name } => {
916                assert_eq!(session, "work");
917                assert_eq!(window, 0);
918                assert_eq!(layout_name, "dev");
919            }
920            _ => panic!("wrong variant"),
921        }
922    }
923
924    #[test]
925    fn request_select_layout_roundtrip() {
926        let req = Request::SelectLayout {
927            session: "work".into(),
928            window: 0,
929            layout: "tiled".into(),
930        };
931        let decoded = roundtrip_request(&req);
932        match decoded {
933            Request::SelectLayout { session, window, layout } => {
934                assert_eq!(session, "work");
935                assert_eq!(window, 0);
936                assert_eq!(layout, "tiled");
937            }
938            _ => panic!("wrong variant"),
939        }
940    }
941
942    #[test]
943    fn response_layout_applied_roundtrip() {
944        let resp = Response::LayoutApplied {
945            pane_ids: vec!["%0".into(), "%1".into(), "%2".into()],
946        };
947        let decoded = roundtrip_response(&resp);
948        match decoded {
949            Response::LayoutApplied { pane_ids } => {
950                assert_eq!(pane_ids, vec!["%0", "%1", "%2"]);
951            }
952            _ => panic!("wrong variant"),
953        }
954    }
955
956    #[test]
957    fn request_new_session_with_layout_roundtrip() {
958        let req = Request::NewSession {
959            name: "work".into(),
960            shell: None,
961            layout: Some("dev".into()),
962            pane_alias: None,
963            template: None,
964        };
965        let decoded = roundtrip_request(&req);
966        match decoded {
967            Request::NewSession { name, shell, layout, pane_alias, template } => {
968                assert_eq!(name, "work");
969                assert!(shell.is_none());
970                assert_eq!(layout, Some("dev".into()));
971                assert!(pane_alias.is_none());
972                assert!(template.is_none());
973            }
974            _ => panic!("wrong variant"),
975        }
976    }
977
978    #[test]
979    fn request_new_window_with_layout_roundtrip() {
980        let req = Request::NewWindow {
981            session: "work".into(),
982            shell: None,
983            layout: Some("dev".into()),
984            pane_alias: None,
985        };
986        let decoded = roundtrip_request(&req);
987        match decoded {
988            Request::NewWindow { session, shell, layout, pane_alias } => {
989                assert_eq!(session, "work");
990                assert!(shell.is_none());
991                assert_eq!(layout, Some("dev".into()));
992                assert!(pane_alias.is_none());
993            }
994            _ => panic!("wrong variant"),
995        }
996    }
997
998    #[test]
999    fn request_save_layout_roundtrip() {
1000        let req = Request::SaveLayout {
1001            session: "work".into(),
1002            window: 0,
1003            name: "dev-layout".into(),
1004        };
1005        let decoded = roundtrip_request(&req);
1006        match decoded {
1007            Request::SaveLayout { session, window, name } => {
1008                assert_eq!(session, "work");
1009                assert_eq!(window, 0);
1010                assert_eq!(name, "dev-layout");
1011            }
1012            _ => panic!("wrong variant"),
1013        }
1014    }
1015
1016    #[test]
1017    fn request_load_layout_roundtrip() {
1018        let req = Request::LoadLayout {
1019            session: "work".into(),
1020            window: 1,
1021            name: "dev-layout".into(),
1022        };
1023        let decoded = roundtrip_request(&req);
1024        match decoded {
1025            Request::LoadLayout { session, window, name } => {
1026                assert_eq!(session, "work");
1027                assert_eq!(window, 1);
1028                assert_eq!(name, "dev-layout");
1029            }
1030            _ => panic!("wrong variant"),
1031        }
1032    }
1033
1034    #[test]
1035    fn request_list_layouts_roundtrip() {
1036        let req = Request::ListLayouts;
1037        let decoded = roundtrip_request(&req);
1038        assert!(matches!(decoded, Request::ListLayouts));
1039    }
1040
1041    #[test]
1042    fn response_layout_saved_roundtrip() {
1043        let resp = Response::LayoutSaved;
1044        let decoded = roundtrip_response(&resp);
1045        assert!(matches!(decoded, Response::LayoutSaved));
1046    }
1047
1048    #[test]
1049    fn response_layout_loaded_roundtrip() {
1050        let resp = Response::LayoutLoaded {
1051            pane_ids: vec!["%0".into(), "%1".into()],
1052        };
1053        let decoded = roundtrip_response(&resp);
1054        match decoded {
1055            Response::LayoutLoaded { pane_ids } => {
1056                assert_eq!(pane_ids, vec!["%0", "%1"]);
1057            }
1058            _ => panic!("wrong variant"),
1059        }
1060    }
1061
1062    #[test]
1063    fn response_layouts_roundtrip() {
1064        let resp = Response::Layouts(vec![
1065            LayoutListEntry { name: "dev".into(), source: LayoutSource::Config },
1066            LayoutListEntry { name: "my-save".into(), source: LayoutSource::Saved },
1067            LayoutListEntry { name: "tiled".into(), source: LayoutSource::Preset },
1068        ]);
1069        let decoded = roundtrip_response(&resp);
1070        match decoded {
1071            Response::Layouts(entries) => {
1072                assert_eq!(entries.len(), 3);
1073                assert_eq!(entries[0].name, "dev");
1074                assert_eq!(entries[0].source, LayoutSource::Config);
1075                assert_eq!(entries[1].name, "my-save");
1076                assert_eq!(entries[1].source, LayoutSource::Saved);
1077                assert_eq!(entries[2].name, "tiled");
1078                assert_eq!(entries[2].source, LayoutSource::Preset);
1079            }
1080            _ => panic!("wrong variant"),
1081        }
1082    }
1083
1084    #[test]
1085    fn request_server_status_roundtrip() {
1086        let req = Request::ServerStatus;
1087        let decoded = roundtrip_request(&req);
1088        assert!(matches!(decoded, Request::ServerStatus));
1089    }
1090
1091    #[test]
1092    fn response_server_status_roundtrip() {
1093        let resp = Response::ServerStatus(ServerStatusInfo {
1094            version: "0.1.0".into(),
1095            uptime_secs: 3600,
1096            sessions: 2,
1097            windows: 3,
1098            panes: 5,
1099            pid: 12345,
1100        });
1101        let decoded = roundtrip_response(&resp);
1102        match decoded {
1103            Response::ServerStatus(info) => {
1104                assert_eq!(info.version, "0.1.0");
1105                assert_eq!(info.uptime_secs, 3600);
1106                assert_eq!(info.sessions, 2);
1107                assert_eq!(info.windows, 3);
1108                assert_eq!(info.panes, 5);
1109                assert_eq!(info.pid, 12345);
1110            }
1111            _ => panic!("wrong variant"),
1112        }
1113    }
1114
1115    #[test]
1116    fn request_new_session_with_pane_alias_roundtrip() {
1117        let req = Request::NewSession {
1118            name: "work".into(),
1119            shell: None,
1120            layout: None,
1121            pane_alias: Some("builder".into()),
1122            template: None,
1123        };
1124        let decoded = roundtrip_request(&req);
1125        match decoded {
1126            Request::NewSession { pane_alias, .. } => {
1127                assert_eq!(pane_alias, Some("builder".into()));
1128            }
1129            _ => panic!("wrong variant"),
1130        }
1131    }
1132
1133    #[test]
1134    fn request_new_session_with_template_roundtrip() {
1135        let req = Request::NewSession {
1136            name: "work".into(),
1137            shell: None,
1138            layout: None,
1139            pane_alias: None,
1140            template: Some("dev".into()),
1141        };
1142        let decoded = roundtrip_request(&req);
1143        match decoded {
1144            Request::NewSession { name, template, layout, .. } => {
1145                assert_eq!(name, "work");
1146                assert_eq!(template, Some("dev".into()));
1147                assert!(layout.is_none());
1148            }
1149            _ => panic!("wrong variant"),
1150        }
1151    }
1152
1153    #[test]
1154    fn request_batch_roundtrip() {
1155        let req = Request::Batch {
1156            requests: vec![
1157                Request::Ping,
1158                Request::ListSessions,
1159                Request::NewSession {
1160                    name: "test".into(),
1161                    shell: None,
1162                    layout: None,
1163                    pane_alias: None,
1164                    template: None,
1165                },
1166            ],
1167        };
1168        let decoded = roundtrip_request(&req);
1169        match decoded {
1170            Request::Batch { requests } => {
1171                assert_eq!(requests.len(), 3);
1172                assert!(matches!(requests[0], Request::Ping));
1173                assert!(matches!(requests[1], Request::ListSessions));
1174                assert!(matches!(requests[2], Request::NewSession { .. }));
1175            }
1176            _ => panic!("wrong variant"),
1177        }
1178    }
1179
1180    #[test]
1181    fn response_batch_result_roundtrip() {
1182        let resp = Response::BatchResult {
1183            responses: vec![
1184                Response::Pong,
1185                Response::Sessions(vec![]),
1186                Response::Error { message: "test error".into() },
1187            ],
1188        };
1189        let decoded = roundtrip_response(&resp);
1190        match decoded {
1191            Response::BatchResult { responses } => {
1192                assert_eq!(responses.len(), 3);
1193                assert!(matches!(responses[0], Response::Pong));
1194                assert!(matches!(responses[1], Response::Sessions(_)));
1195                match &responses[2] {
1196                    Response::Error { message } => assert_eq!(message, "test error"),
1197                    _ => panic!("expected Error"),
1198                }
1199            }
1200            _ => panic!("wrong variant"),
1201        }
1202    }
1203
1204    #[test]
1205    fn request_batch_json_roundtrip() {
1206        // Verify Request/Response can serialize/deserialize via JSON for batch CLI
1207        let requests = vec![Request::Ping, Request::ListSessions];
1208        let json = serde_json::to_string(&requests).unwrap();
1209        let decoded: Vec<Request> = serde_json::from_str(&json).unwrap();
1210        assert_eq!(decoded.len(), 2);
1211        assert!(matches!(decoded[0], Request::Ping));
1212        assert!(matches!(decoded[1], Request::ListSessions));
1213    }
1214
1215    #[test]
1216    fn response_batch_result_json_roundtrip() {
1217        let responses = vec![Response::Pong, Response::Sessions(vec![])];
1218        let json = serde_json::to_string_pretty(&responses).unwrap();
1219        let decoded: Vec<Response> = serde_json::from_str(&json).unwrap();
1220        assert_eq!(decoded.len(), 2);
1221        assert!(matches!(decoded[0], Response::Pong));
1222        assert!(matches!(decoded[1], Response::Sessions(_)));
1223    }
1224
1225    #[test]
1226    fn request_split_pane_with_alias_roundtrip() {
1227        let req = Request::SplitPane {
1228            target: "%0".into(),
1229            horizontal: false,
1230            shell: None,
1231            pane_alias: Some("test-pane".into()),
1232        };
1233        let decoded = roundtrip_request(&req);
1234        match decoded {
1235            Request::SplitPane { pane_alias, .. } => {
1236                assert_eq!(pane_alias, Some("test-pane".into()));
1237            }
1238            _ => panic!("wrong variant"),
1239        }
1240    }
1241
1242    #[test]
1243    fn request_new_window_with_pane_alias_roundtrip() {
1244        let req = Request::NewWindow {
1245            session: "work".into(),
1246            shell: None,
1247            layout: None,
1248            pane_alias: Some("runner".into()),
1249        };
1250        let decoded = roundtrip_request(&req);
1251        match decoded {
1252            Request::NewWindow { pane_alias, .. } => {
1253                assert_eq!(pane_alias, Some("runner".into()));
1254            }
1255            _ => panic!("wrong variant"),
1256        }
1257    }
1258
1259    #[test]
1260    fn request_wait_with_new_flags_roundtrip() {
1261        let req = Request::Wait {
1262            pane_id: "%0".into(),
1263            pattern: "ready|done".into(),
1264            timeout_secs: Some(10),
1265            return_match: true,
1266            capture_on_match: true,
1267            scrollback: true,
1268        };
1269        let decoded = roundtrip_request(&req);
1270        match decoded {
1271            Request::Wait {
1272                pane_id,
1273                pattern,
1274                timeout_secs,
1275                return_match,
1276                capture_on_match,
1277                scrollback,
1278            } => {
1279                assert_eq!(pane_id, "%0");
1280                assert_eq!(pattern, "ready|done");
1281                assert_eq!(timeout_secs, Some(10));
1282                assert!(return_match);
1283                assert!(capture_on_match);
1284                assert!(scrollback);
1285            }
1286            _ => panic!("wrong variant"),
1287        }
1288    }
1289
1290    #[test]
1291    fn response_wait_matched_with_details_roundtrip() {
1292        let resp = Response::WaitMatched {
1293            matched: Some("done".into()),
1294            line: Some("build done successfully".into()),
1295            capture: Some(CaptureResult {
1296                pane_id: "%0".into(),
1297                lines: vec!["build done successfully".into()],
1298                scrollback_lines: vec![],
1299                cursor: CursorPos { row: 1, col: 0 },
1300                size: (80, 24),
1301                process: ProcessInfo { pid: 1234, running: true, exit_code: None },
1302                title: "cmd.exe".into(),
1303            }),
1304        };
1305        let decoded = roundtrip_response(&resp);
1306        match decoded {
1307            Response::WaitMatched { matched, line, capture } => {
1308                assert_eq!(matched, Some("done".into()));
1309                assert_eq!(line, Some("build done successfully".into()));
1310                assert!(capture.is_some());
1311                assert_eq!(capture.unwrap().pane_id, "%0");
1312            }
1313            _ => panic!("wrong variant"),
1314        }
1315    }
1316
1317    #[test]
1318    fn request_capture_with_filter_roundtrip() {
1319        let req = Request::Capture {
1320            pane_id: "%0".into(),
1321            json: true,
1322            scrollback: Some(ScrollbackRange::All),
1323            filter: Some(CaptureFilter::LastCommand { strip_prompt: true }),
1324        };
1325        let decoded = roundtrip_request(&req);
1326        match decoded {
1327            Request::Capture { filter, .. } => {
1328                match filter {
1329                    Some(CaptureFilter::LastCommand { strip_prompt }) => assert!(strip_prompt),
1330                    _ => panic!("expected LastCommand filter"),
1331                }
1332            }
1333            _ => panic!("wrong variant"),
1334        }
1335    }
1336
1337    #[test]
1338    fn request_exec_with_filter_roundtrip() {
1339        let req = Request::Exec {
1340            pane_id: "%0".into(),
1341            command: "dir".into(),
1342            wait: true,
1343            capture: true,
1344            scrollback: Some(ScrollbackRange::All),
1345            filter: Some(CaptureFilter::LastCommand { strip_prompt: false }),
1346        };
1347        let decoded = roundtrip_request(&req);
1348        match decoded {
1349            Request::Exec { filter, .. } => {
1350                match filter {
1351                    Some(CaptureFilter::LastCommand { strip_prompt }) => assert!(!strip_prompt),
1352                    _ => panic!("expected LastCommand filter"),
1353                }
1354            }
1355            _ => panic!("wrong variant"),
1356        }
1357    }
1358}