1use agentmux_common::ipc::{ErrorCode, Event};
5
6use crate::state::State;
7
8
9use crate::state::WorkspaceRecord;
10
11pub(super) fn handle_create_workspace(state: &mut State, name: String) -> Vec<Event> {
19 let workspace_id = uuid::Uuid::new_v4().to_string();
20 state.workspaces.insert(
21 workspace_id.clone(),
22 WorkspaceRecord {
23 workspace_id: workspace_id.clone(),
24 name: name.clone(),
25 tab_ids: Vec::new(),
26 active_tab_id: None,
27 },
28 );
29 let v = state.bump_version();
30 vec![Event::WorkspaceCreated {
31 workspace_id,
32 name,
33 version: v,
34 }]
35}
36
37pub(super) fn handle_delete_workspace(
54 state: &mut State,
55 workspace_id: String,
56 _force: bool,
57) -> Vec<Event> {
58 let Some(removed) = state.workspaces.remove(&workspace_id) else {
59 return Vec::new();
60 };
61 let mut cascaded_block_ids: Vec<String> = Vec::new();
66 for tab_id in &removed.tab_ids {
67 if let Some(tab) = state.tabs.remove(tab_id) {
68 cascaded_block_ids.extend(tab.block_ids.iter().cloned());
69 for block_id in &tab.block_ids {
70 state.blocks.remove(block_id);
71 }
72 }
73 }
74 let dropped_window_ids: Vec<String> = state
80 .windows
81 .iter()
82 .filter(|(_, w)| w.workspace_id == workspace_id)
83 .map(|(id, _)| id.clone())
84 .collect();
85 for id in &dropped_window_ids {
86 state.windows.remove(id);
87 }
88 let mut events = Vec::with_capacity(1 + dropped_window_ids.len());
89 let v = state.bump_version();
90 events.push(Event::WorkspaceDeleted {
91 workspace_id,
92 block_ids: cascaded_block_ids,
93 version: v,
94 });
95 for window_id in dropped_window_ids {
96 let v = state.bump_version();
97 events.push(Event::SrvWindowClosed { window_id, version: v });
98 }
99 events
100}
101
102pub(super) fn handle_rename_workspace(state: &mut State, workspace_id: String, name: String) -> Vec<Event> {
105 let Some(workspace) = state.workspaces.get_mut(&workspace_id) else {
106 let v = state.bump_version();
107 return vec![Event::Error {
108 code: ErrorCode::InvalidCommand,
109 message: format!("RenameWorkspace: workspace not found: {}", workspace_id),
110 fatal: false,
111 version: v,
112 }];
113 };
114 if workspace.name == name {
115 return Vec::new();
116 }
117 workspace.name = name.clone();
118 let v = state.bump_version();
119 vec![Event::WorkspaceRenamed {
120 workspace_id,
121 name,
122 version: v,
123 }]
124}
125
126pub(super) fn handle_update_workspace_meta(
133 state: &mut State,
134 workspace_id: String,
135 meta_patch: serde_json::Value,
136) -> Vec<Event> {
137 if !state.workspaces.contains_key(&workspace_id) {
138 let v = state.bump_version();
139 return vec![Event::Error {
140 code: ErrorCode::InvalidCommand,
141 message: format!(
142 "UpdateWorkspaceMeta: workspace not found: {}",
143 workspace_id
144 ),
145 fatal: false,
146 version: v,
147 }];
148 }
149 let v = state.bump_version();
150 vec![Event::WorkspaceMetaUpdated {
151 workspace_id,
152 meta_patch,
153 version: v,
154 }]
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::reducer::test_support::*;
161 use crate::reducer::update;
162 use agentmux_common::ipc::Command;
163
164 #[test]
165 fn create_workspace_inserts_record_and_emits_event() {
166 let mut state = State::default();
167 let events = update(
168 &mut state,
169 Command::CreateWorkspace {
170 name: "myws".into(),
171 },
172 &ctx(1),
173 );
174 assert_eq!(events.len(), 1);
175 let Event::WorkspaceCreated {
176 workspace_id, name, ..
177 } = &events[0]
178 else {
179 panic!("expected WorkspaceCreated, got {:?}", events[0]);
180 };
181 assert_eq!(name, "myws");
182 assert!(state.workspaces.contains_key(workspace_id));
183 assert_eq!(state.workspaces[workspace_id].name, "myws");
184 }
185
186 #[test]
187 fn delete_workspace_removes_record_and_emits_event() {
188 let mut state = State::default();
189 let events = update(
190 &mut state,
191 Command::CreateWorkspace {
192 name: "to-delete".into(),
193 },
194 &ctx(1),
195 );
196 let Event::WorkspaceCreated { workspace_id, .. } = &events[0] else {
197 panic!();
198 };
199 let ws_id = workspace_id.clone();
200 let events = update(
201 &mut state,
202 Command::DeleteWorkspace {
203 workspace_id: ws_id.clone(),
204 force: false,
205 },
206 &ctx(2),
207 );
208 assert_eq!(events.len(), 1);
209 assert!(matches!(
210 &events[0],
211 Event::WorkspaceDeleted { workspace_id, .. } if workspace_id == &ws_id
212 ));
213 assert!(!state.workspaces.contains_key(&ws_id));
214 }
215
216 #[test]
217 fn delete_workspace_unknown_is_silent_no_op() {
218 let mut state = State::default();
219 let events = update(
220 &mut state,
221 Command::DeleteWorkspace {
222 workspace_id: "does-not-exist".into(),
223 force: false,
224 },
225 &ctx(1),
226 );
227 assert!(events.is_empty());
228 }
229
230 #[test]
231 fn delete_workspace_cascades_tabs() {
232 let mut state = State::default();
233 let ws_id = create_workspace(&mut state, "w");
234 let _ = update(
235 &mut state,
236 Command::CreateTab {
237 workspace_id: ws_id.clone(),
238 name: "t1".into(),
239 },
240 &ctx(1),
241 );
242 let _ = update(
243 &mut state,
244 Command::CreateTab {
245 workspace_id: ws_id.clone(),
246 name: "t2".into(),
247 },
248 &ctx(2),
249 );
250 assert_eq!(state.tabs.len(), 2);
251 let events = update(
252 &mut state,
253 Command::DeleteWorkspace {
254 workspace_id: ws_id.clone(),
255 force: false,
256 },
257 &ctx(3),
258 );
259 assert!(matches!(&events[0], Event::WorkspaceDeleted { .. }));
260 assert!(state.tabs.is_empty());
261 assert!(!state.workspaces.contains_key(&ws_id));
262 }
263
264 #[test]
265 fn delete_workspace_emits_cascaded_block_ids_across_all_tabs() {
266 let mut state = State::default();
267 let ws_id = create_workspace(&mut state, "w");
268 let tab1_id = create_tab(&mut state, &ws_id, "t1");
269 let tab2_id = create_tab(&mut state, &ws_id, "t2");
270 let mut expected = Vec::new();
271 for (i, tab_id) in [&tab1_id, &tab2_id].into_iter().enumerate() {
272 for j in 0..2 {
273 let block_id = match &update(
274 &mut state,
275 Command::CreateBlock { tab_id: tab_id.clone(), meta: serde_json::Value::Null },
276 &ctx(10 + (i * 2 + j) as u64),
277 )[0] {
278 Event::BlockCreated { block_id, .. } => block_id.clone(),
279 other => panic!("expected BlockCreated, got {:?}", other),
280 };
281 expected.push(block_id);
282 }
283 }
284 assert_eq!(expected.len(), 4);
285 let events = update(
286 &mut state,
287 Command::DeleteWorkspace { workspace_id: ws_id.clone(), force: false },
288 &ctx(20),
289 );
290 match &events[0] {
291 Event::WorkspaceDeleted { block_ids, .. } => {
292 assert_eq!(block_ids.len(), 4);
293 for b in &expected {
294 assert!(block_ids.contains(b));
295 }
296 }
297 other => panic!("expected WorkspaceDeleted, got {:?}", other),
298 }
299 }
300
301 #[test]
302 fn delete_workspace_cascades_through_tabs_to_blocks() {
303 let mut state = State::default();
304 let ws_id = create_workspace(&mut state, "w");
305 let tab_id = create_tab(&mut state, &ws_id, "t");
306 let _ = update(
307 &mut state,
308 Command::CreateBlock { tab_id: tab_id.clone(), meta: serde_json::Value::Null },
309 &ctx(2),
310 );
311 let _ = update(
312 &mut state,
313 Command::CreateBlock { tab_id, meta: serde_json::Value::Null },
314 &ctx(3),
315 );
316 assert_eq!(state.blocks.len(), 2);
317 let _ = update(
318 &mut state,
319 Command::DeleteWorkspace { workspace_id: ws_id, force: false },
320 &ctx(4),
321 );
322 assert!(state.blocks.is_empty());
323 assert!(state.tabs.is_empty());
324 }
325
326 #[test]
327 fn delete_workspace_drops_pointing_windows_and_emits_events() {
328 let mut state = State::default();
329 let ws_a = create_workspace(&mut state, "a");
330 let ws_b = create_workspace(&mut state, "b");
331 let _ = update(
332 &mut state,
333 Command::CreateWindow {
334 window_id: "win-a".into(),
335 workspace_id: ws_a.clone(),
336 },
337 &ctx(2),
338 );
339 let _ = update(
340 &mut state,
341 Command::CreateWindow {
342 window_id: "win-b".into(),
343 workspace_id: ws_b.clone(),
344 },
345 &ctx(3),
346 );
347 let events = update(
350 &mut state,
351 Command::DeleteWorkspace { workspace_id: ws_a, force: false },
352 &ctx(4),
353 );
354 assert!(matches!(&events[0], Event::WorkspaceDeleted { .. }));
355 assert!(events.iter().any(|e| matches!(
356 e,
357 Event::SrvWindowClosed { window_id, .. } if window_id == "win-a"
358 )));
359 assert!(!events.iter().any(|e| matches!(
361 e,
362 Event::SrvWindowClosed { window_id, .. } if window_id == "win-b"
363 )));
364 assert!(!state.windows.contains_key("win-a"));
365 assert_eq!(state.windows["win-b"].workspace_id, ws_b);
366 }
367}