Skip to main content

agentmux_srv/reducer/
snapshot.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4use agentmux_common::ipc::Event;
5
6use crate::state::State;
7
8
9pub(super) fn handle_get_srv_snapshot(state: &mut State) -> Vec<Event> {
10    let v = state.bump_version();
11    let mut workspaces: Vec<(String, String)> = state
12        .workspaces
13        .values()
14        .map(|w| (w.workspace_id.clone(), w.name.clone()))
15        .collect();
16    // Stable ordering for diffability — reducer state is HashMap so
17    // iteration order is non-deterministic.
18    workspaces.sort_by(|a, b| a.0.cmp(&b.0));
19    let mut tabs: Vec<(String, String, String)> = state
20        .tabs
21        .values()
22        .map(|t| (t.tab_id.clone(), t.workspace_id.clone(), t.name.clone()))
23        .collect();
24    tabs.sort_by(|a, b| a.0.cmp(&b.0));
25    let mut active_tabs: Vec<(String, String)> = state
26        .workspaces
27        .values()
28        .filter_map(|w| {
29            w.active_tab_id
30                .as_ref()
31                .map(|t| (w.workspace_id.clone(), t.clone()))
32        })
33        .collect();
34    active_tabs.sort_by(|a, b| a.0.cmp(&b.0));
35    let mut blocks: Vec<(String, String)> = state
36        .blocks
37        .values()
38        .map(|b| (b.block_id.clone(), b.tab_id.clone()))
39        .collect();
40    blocks.sort_by(|a, b| a.0.cmp(&b.0));
41    vec![Event::SrvSnapshot {
42        version: v,
43        lifecycle: state.lifecycle,
44        workspaces,
45        tabs,
46        active_tabs,
47        blocks,
48    }]
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::reducer::test_support::*;
55    use crate::reducer::update;
56    use agentmux_common::ipc::Command;
57
58    #[test]
59    fn get_srv_snapshot_returns_lifecycle_and_bumps_version() {
60        let mut state = State::default();
61        let v0 = state.event_version;
62        let events = update(&mut state, Command::GetSrvSnapshot, &ctx(1));
63        assert_eq!(events.len(), 1);
64        let Event::SrvSnapshot { version, lifecycle, .. } = events[0].clone() else {
65            panic!("expected SrvSnapshot, got {:?}", events[0]);
66        };
67        assert_eq!(lifecycle, agentmux_common::ipc::LifecyclePhase::Starting);
68        assert!(version > v0);
69    }
70
71    #[test]
72    fn snapshot_includes_workspaces_sorted_by_id() {
73        let mut state = State::default();
74        let _ = update(
75            &mut state,
76            Command::CreateWorkspace { name: "a".into() },
77            &ctx(1),
78        );
79        let _ = update(
80            &mut state,
81            Command::CreateWorkspace { name: "b".into() },
82            &ctx(2),
83        );
84        let events = update(&mut state, Command::GetSrvSnapshot, &ctx(3));
85        let Event::SrvSnapshot { workspaces, .. } = &events[0] else {
86            panic!();
87        };
88        assert_eq!(workspaces.len(), 2);
89        // Sorted by id; verify ordering deterministic (ascending).
90        assert!(workspaces[0].0 < workspaces[1].0);
91    }
92
93    #[test]
94    fn snapshot_includes_blocks() {
95        let mut state = State::default();
96        let ws_id = create_workspace(&mut state, "w");
97        let tab_id = create_tab(&mut state, &ws_id, "t");
98        let _ = update(
99            &mut state,
100            Command::CreateBlock { tab_id, meta: serde_json::Value::Null },
101            &ctx(2),
102        );
103        let events = update(&mut state, Command::GetSrvSnapshot, &ctx(3));
104        let Event::SrvSnapshot { blocks, .. } = &events[0] else {
105            panic!("expected SrvSnapshot");
106        };
107        assert_eq!(blocks.len(), 1);
108    }
109
110    #[test]
111    fn snapshot_includes_tabs_and_active_tabs() {
112        let mut state = State::default();
113        let ws_id = create_workspace(&mut state, "w");
114        let _ = update(
115            &mut state,
116            Command::CreateTab {
117                workspace_id: ws_id.clone(),
118                name: "t1".into(),
119            },
120            &ctx(1),
121        );
122        let events = update(&mut state, Command::GetSrvSnapshot, &ctx(2));
123        let Event::SrvSnapshot {
124            tabs, active_tabs, ..
125        } = &events[0]
126        else {
127            panic!("expected SrvSnapshot");
128        };
129        assert_eq!(tabs.len(), 1);
130        assert_eq!(active_tabs.len(), 1);
131        assert_eq!(active_tabs[0].0, ws_id);
132    }
133}