1use agentmux_common::ipc::{ErrorCode, Event};
5
6use crate::state::State;
7
8
9use crate::state::WindowRecord;
10
11pub(super) fn handle_create_window(
17 state: &mut State,
18 window_id: String,
19 workspace_id: String,
20) -> Vec<Event> {
21 if !state.workspaces.contains_key(&workspace_id) {
22 let v = state.bump_version();
23 return vec![Event::Error {
24 code: ErrorCode::InvalidCommand,
25 message: format!("CreateWindow: workspace not found: {}", workspace_id),
26 fatal: false,
27 version: v,
28 }];
29 }
30 if let Some(existing) = state.windows.get(&window_id) {
31 if existing.workspace_id == workspace_id {
32 return Vec::new();
33 }
34 }
35 state.windows.insert(
36 window_id.clone(),
37 WindowRecord {
38 window_id: window_id.clone(),
39 workspace_id: workspace_id.clone(),
40 },
41 );
42 let v = state.bump_version();
43 vec![Event::SrvWindowOpened {
44 window_id,
45 workspace_id,
46 version: v,
47 }]
48}
49
50pub(super) fn handle_close_window_internal(state: &mut State, window_id: String) -> Vec<Event> {
53 if state.windows.remove(&window_id).is_none() {
54 return Vec::new();
55 }
56 let v = state.bump_version();
57 vec![Event::SrvWindowClosed {
58 window_id,
59 version: v,
60 }]
61}
62
63pub(super) fn handle_switch_workspace(
67 state: &mut State,
68 window_id: String,
69 workspace_id: String,
70) -> Vec<Event> {
71 if !state.workspaces.contains_key(&workspace_id) {
72 let v = state.bump_version();
73 return vec![Event::Error {
74 code: ErrorCode::InvalidCommand,
75 message: format!(
76 "SwitchWorkspace: destination workspace not found: {}",
77 workspace_id
78 ),
79 fatal: false,
80 version: v,
81 }];
82 }
83 let Some(window) = state.windows.get_mut(&window_id) else {
84 let v = state.bump_version();
85 return vec![Event::Error {
86 code: ErrorCode::InvalidCommand,
87 message: format!("SwitchWorkspace: window not found: {}", window_id),
88 fatal: false,
89 version: v,
90 }];
91 };
92 if window.workspace_id == workspace_id {
93 return Vec::new();
94 }
95 window.workspace_id = workspace_id.clone();
96 let v = state.bump_version();
97 vec![Event::SrvWindowWorkspaceChanged {
98 window_id,
99 workspace_id,
100 version: v,
101 }]
102}
103
104pub(super) fn handle_update_window_meta(
127 state: &mut State,
128 window_id: String,
129 meta_patch: serde_json::Value,
130) -> Vec<Event> {
131 if !state.windows.contains_key(&window_id) {
132 let v = state.bump_version();
133 return vec![Event::Error {
134 code: ErrorCode::InvalidCommand,
135 message: format!("UpdateWindowMeta: window not found: {}", window_id),
136 fatal: false,
137 version: v,
138 }];
139 }
140 let v = state.bump_version();
141 vec![Event::WindowMetaUpdated {
142 window_id,
143 meta_patch,
144 version: v,
145 }]
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::reducer::test_support::*;
152 use crate::reducer::update;
153 use agentmux_common::ipc::Command;
154
155 #[test]
156 fn create_window_validates_workspace_exists() {
157 let mut state = State::default();
158 let events = update(
159 &mut state,
160 Command::CreateWindow {
161 window_id: "win-1".into(),
162 workspace_id: "no-such-ws".into(),
163 },
164 &ctx(1),
165 );
166 assert!(matches!(&events[0], Event::Error { .. }));
167 assert!(state.windows.is_empty());
168 }
169
170 #[test]
171 fn create_window_inserts_record_and_emits_event() {
172 let mut state = State::default();
173 let ws_id = create_workspace(&mut state, "w");
174 let events = update(
175 &mut state,
176 Command::CreateWindow {
177 window_id: "win-1".into(),
178 workspace_id: ws_id.clone(),
179 },
180 &ctx(2),
181 );
182 assert!(matches!(
183 &events[0],
184 Event::SrvWindowOpened { window_id, workspace_id, .. }
185 if window_id == "win-1" && *workspace_id == ws_id
186 ));
187 assert_eq!(state.windows["win-1"].workspace_id, ws_id);
188 }
189
190 #[test]
191 fn create_window_idempotent_on_same_workspace() {
192 let mut state = State::default();
193 let ws_id = create_workspace(&mut state, "w");
194 let _ = update(
195 &mut state,
196 Command::CreateWindow {
197 window_id: "win-1".into(),
198 workspace_id: ws_id.clone(),
199 },
200 &ctx(2),
201 );
202 let events = update(
203 &mut state,
204 Command::CreateWindow {
205 window_id: "win-1".into(),
206 workspace_id: ws_id,
207 },
208 &ctx(3),
209 );
210 assert!(events.is_empty());
211 }
212
213 #[test]
214 fn close_window_internal_removes_record() {
215 let mut state = State::default();
216 let ws_id = create_workspace(&mut state, "w");
217 let _ = update(
218 &mut state,
219 Command::CreateWindow {
220 window_id: "win-1".into(),
221 workspace_id: ws_id,
222 },
223 &ctx(2),
224 );
225 let events = update(
226 &mut state,
227 Command::CloseWindowInternal {
228 window_id: "win-1".into(),
229 },
230 &ctx(3),
231 );
232 assert!(matches!(&events[0], Event::SrvWindowClosed { .. }));
233 assert!(state.windows.is_empty());
234 }
235
236 #[test]
237 fn close_window_internal_silent_on_missing() {
238 let mut state = State::default();
239 let events = update(
240 &mut state,
241 Command::CloseWindowInternal {
242 window_id: "ghost".into(),
243 },
244 &ctx(1),
245 );
246 assert!(events.is_empty());
247 }
248
249 #[test]
250 fn update_window_meta_errors_on_unknown_window() {
251 let mut state = State::default();
256 let events = update(
257 &mut state,
258 Command::UpdateWindowMeta {
259 window_id: "00000000-dead-beef-0000-000000000000".into(),
260 meta_patch: serde_json::json!({ "window:displayname": "phantom" }),
261 },
262 &ctx(1),
263 );
264 assert!(
265 matches!(&events[0], Event::Error { message, .. } if message.contains("window not found")),
266 "expected window-not-found error, got {:?}",
267 events
268 );
269 }
270
271 #[test]
272 fn update_window_meta_emits_event_for_known_window() {
273 let mut state = State::default();
274 let ws_id = create_workspace(&mut state, "a");
275 let _ = update(
276 &mut state,
277 Command::CreateWindow {
278 window_id: "win-1".into(),
279 workspace_id: ws_id,
280 },
281 &ctx(2),
282 );
283 let events = update(
284 &mut state,
285 Command::UpdateWindowMeta {
286 window_id: "win-1".into(),
287 meta_patch: serde_json::json!({ "window:displayname": "named" }),
288 },
289 &ctx(3),
290 );
291 assert!(
292 matches!(&events[0], Event::WindowMetaUpdated { window_id, .. } if window_id == "win-1"),
293 "expected WindowMetaUpdated, got {:?}",
294 events
295 );
296 }
297
298 #[test]
299 fn switch_workspace_updates_window_pointer() {
300 let mut state = State::default();
301 let ws_a = create_workspace(&mut state, "a");
302 let ws_b = create_workspace(&mut state, "b");
303 let _ = update(
304 &mut state,
305 Command::CreateWindow {
306 window_id: "win-1".into(),
307 workspace_id: ws_a.clone(),
308 },
309 &ctx(2),
310 );
311 let events = update(
312 &mut state,
313 Command::SwitchWorkspace {
314 window_id: "win-1".into(),
315 workspace_id: ws_b.clone(),
316 },
317 &ctx(3),
318 );
319 assert!(matches!(
320 &events[0],
321 Event::SrvWindowWorkspaceChanged { workspace_id, .. } if *workspace_id == ws_b
322 ));
323 assert_eq!(state.windows["win-1"].workspace_id, ws_b);
324 }
325
326 #[test]
327 fn switch_workspace_validates_window_and_destination() {
328 let mut state = State::default();
329 let ws_id = create_workspace(&mut state, "a");
330 let events = update(
332 &mut state,
333 Command::SwitchWorkspace {
334 window_id: "ghost".into(),
335 workspace_id: ws_id.clone(),
336 },
337 &ctx(2),
338 );
339 assert!(matches!(&events[0], Event::Error { .. }));
340 let _ = update(
342 &mut state,
343 Command::CreateWindow {
344 window_id: "win-1".into(),
345 workspace_id: ws_id,
346 },
347 &ctx(3),
348 );
349 let events = update(
350 &mut state,
351 Command::SwitchWorkspace {
352 window_id: "win-1".into(),
353 workspace_id: "no-such-ws".into(),
354 },
355 &ctx(4),
356 );
357 assert!(matches!(&events[0], Event::Error { .. }));
358 }
359
360 #[test]
361 fn switch_workspace_no_op_when_already_pointing() {
362 let mut state = State::default();
363 let ws_id = create_workspace(&mut state, "a");
364 let _ = update(
365 &mut state,
366 Command::CreateWindow {
367 window_id: "win-1".into(),
368 workspace_id: ws_id.clone(),
369 },
370 &ctx(2),
371 );
372 let events = update(
373 &mut state,
374 Command::SwitchWorkspace {
375 window_id: "win-1".into(),
376 workspace_id: ws_id,
377 },
378 &ctx(3),
379 );
380 assert!(events.is_empty());
381 }
382}