agent_tui/daemon/
router.rs

1//! Request routing for the daemon server.
2//!
3//! The Router maps RPC method names to their handlers, keeping routing logic
4//! separate from server infrastructure. This enables independent testing of
5//! routing without server setup.
6
7use crate::ipc::{RpcRequest, RpcResponse};
8use serde_json::json;
9
10use super::usecase_container::UseCaseContainer;
11use crate::daemon::handlers;
12use crate::daemon::repository::SessionRepository;
13
14/// Routes RPC requests to appropriate handlers.
15///
16/// The Router is responsible for:
17/// - Mapping method names to handlers
18/// - Delegating to use cases via handlers
19/// - Returning appropriate error responses for unknown methods
20///
21/// This design makes the server a "humble object" with minimal logic.
22pub struct Router<'a, R: SessionRepository + 'static> {
23    usecases: &'a UseCaseContainer<R>,
24}
25
26impl<'a, R: SessionRepository + 'static> Router<'a, R> {
27    /// Create a new router with the given use case container.
28    pub fn new(usecases: &'a UseCaseContainer<R>) -> Self {
29        Self { usecases }
30    }
31
32    /// Route an RPC request to the appropriate handler.
33    ///
34    /// Returns the RPC response from the handler, or an error response
35    /// if the method is not found or deprecated.
36    pub fn route(&self, request: RpcRequest) -> RpcResponse {
37        match request.method.as_str() {
38            "ping" => RpcResponse::success(request.id, json!({ "pong": true })),
39
40            "health" => {
41                handlers::diagnostics::handle_health_uc(&self.usecases.diagnostics.health, request)
42            }
43
44            "metrics" => handlers::diagnostics::handle_metrics_uc(
45                &self.usecases.diagnostics.metrics,
46                request,
47            ),
48
49            // Session handlers using use cases
50            "spawn" => handlers::session::handle_spawn(&self.usecases.session.spawn, request),
51            "kill" => handlers::session::handle_kill(&self.usecases.session.kill, request),
52            "restart" => handlers::session::handle_restart(&self.usecases.session.restart, request),
53            "sessions" => {
54                handlers::session::handle_sessions(&self.usecases.session.sessions, request)
55            }
56            "resize" => handlers::session::handle_resize(&self.usecases.session.resize, request),
57            "attach" => handlers::session::handle_attach(&self.usecases.session.attach, request),
58            "cleanup" => handlers::session::handle_cleanup(&self.usecases.session.cleanup, request),
59            "assert" => handlers::session::handle_assert(&self.usecases.session.assert, request),
60
61            // Element handlers - using use cases
62            "snapshot" => {
63                handlers::elements::handle_snapshot_uc(&self.usecases.elements.snapshot, request)
64            }
65            "accessibility_snapshot" => handlers::elements::handle_accessibility_snapshot_uc(
66                &self.usecases.elements.accessibility_snapshot,
67                request,
68            ),
69            "click" => handlers::elements::handle_click_uc(&self.usecases.elements.click, request),
70            "dbl_click" => {
71                handlers::elements::handle_dbl_click_uc(&self.usecases.elements.dbl_click, request)
72            }
73            "fill" => handlers::elements::handle_fill_uc(&self.usecases.elements.fill, request),
74            "find" => handlers::elements::handle_find_uc(&self.usecases.elements.find, request),
75            "count" => handlers::elements::handle_count_uc(&self.usecases.elements.count, request),
76            "scroll" => {
77                handlers::elements::handle_scroll_uc(&self.usecases.elements.scroll, request)
78            }
79            "scroll_into_view" => handlers::elements::handle_scroll_into_view_uc(
80                &self.usecases.elements.scroll_into_view,
81                request,
82            ),
83            "get_text" => {
84                handlers::elements::handle_get_text_uc(&self.usecases.elements.get_text, request)
85            }
86            "get_value" => {
87                handlers::elements::handle_get_value_uc(&self.usecases.elements.get_value, request)
88            }
89            "is_visible" => handlers::elements::handle_is_visible_uc(
90                &self.usecases.elements.is_visible,
91                request,
92            ),
93            "is_focused" => handlers::elements::handle_is_focused_uc(
94                &self.usecases.elements.is_focused,
95                request,
96            ),
97            "is_enabled" => handlers::elements::handle_is_enabled_uc(
98                &self.usecases.elements.is_enabled,
99                request,
100            ),
101            "is_checked" => handlers::elements::handle_is_checked_uc(
102                &self.usecases.elements.is_checked,
103                request,
104            ),
105            "get_focused" => handlers::elements::handle_get_focused_uc(
106                &self.usecases.elements.get_focused,
107                request,
108            ),
109            "get_title" => {
110                handlers::elements::handle_get_title_uc(&self.usecases.elements.get_title, request)
111            }
112            "focus" => handlers::elements::handle_focus_uc(&self.usecases.elements.focus, request),
113            "clear" => handlers::elements::handle_clear_uc(&self.usecases.elements.clear, request),
114            "select_all" => handlers::elements::handle_select_all_uc(
115                &self.usecases.elements.select_all,
116                request,
117            ),
118            "toggle" => {
119                handlers::elements::handle_toggle_uc(&self.usecases.elements.toggle, request)
120            }
121            "select" => {
122                handlers::elements::handle_select_uc(&self.usecases.elements.select, request)
123            }
124            "multiselect" => handlers::elements::handle_multiselect_uc(
125                &self.usecases.elements.multiselect,
126                request,
127            ),
128
129            // Input handlers - keystroke and type use use cases
130            "keystroke" => {
131                handlers::input::handle_keystroke_uc(&self.usecases.input.keystroke, request)
132            }
133            "keydown" => handlers::input::handle_keydown_uc(&self.usecases.input.keydown, request),
134            "keyup" => handlers::input::handle_keyup_uc(&self.usecases.input.keyup, request),
135            "type" => handlers::input::handle_type_uc(&self.usecases.input.type_text, request),
136
137            // Wait handler using use case
138            "wait" => handlers::wait::handle_wait_uc(&self.usecases.wait, request),
139
140            // Recording handlers - using use cases
141            "record_start" => handlers::recording::handle_record_start_uc(
142                &self.usecases.recording.record_start,
143                request,
144            ),
145            "record_stop" => handlers::recording::handle_record_stop_uc(
146                &self.usecases.recording.record_stop,
147                request,
148            ),
149            "record_status" => handlers::recording::handle_record_status_uc(
150                &self.usecases.recording.record_status,
151                request,
152            ),
153
154            // Diagnostics handlers - using use cases
155            "trace" => {
156                handlers::diagnostics::handle_trace_uc(&self.usecases.diagnostics.trace, request)
157            }
158            "console" => handlers::diagnostics::handle_console_uc(
159                &self.usecases.diagnostics.console,
160                request,
161            ),
162            "errors" => {
163                handlers::diagnostics::handle_errors_uc(&self.usecases.diagnostics.errors, request)
164            }
165            "pty_read" => handlers::diagnostics::handle_pty_read_uc(
166                &self.usecases.diagnostics.pty_read,
167                request,
168            ),
169            "pty_write" => handlers::diagnostics::handle_pty_write_uc(
170                &self.usecases.diagnostics.pty_write,
171                request,
172            ),
173
174            "screen" => RpcResponse::error(
175                request.id,
176                -32601,
177                "Method 'screen' is deprecated. Use 'snapshot' with strip_ansi=true instead.",
178            ),
179
180            _ => RpcResponse::error(
181                request.id,
182                -32601,
183                &format!("Method not found: {}", request.method),
184            ),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::daemon::metrics::DaemonMetrics;
193    use crate::daemon::session::SessionManager;
194    use std::sync::Arc;
195    use std::sync::atomic::AtomicUsize;
196    use std::time::Instant;
197
198    fn create_test_usecases() -> UseCaseContainer<SessionManager> {
199        let session_manager = Arc::new(SessionManager::new());
200        let metrics = Arc::new(DaemonMetrics::new());
201        let start_time = Instant::now();
202        let active_connections = Arc::new(AtomicUsize::new(0));
203        UseCaseContainer::new(session_manager, metrics, start_time, active_connections)
204    }
205
206    #[test]
207    fn test_router_ping_returns_pong() {
208        let usecases = create_test_usecases();
209        let router = Router::new(&usecases);
210
211        let request = RpcRequest::new(1, "ping".to_string(), None);
212        let response = router.route(request);
213
214        let json_str = serde_json::to_string(&response).unwrap();
215        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
216
217        assert!(parsed.get("error").is_none() || parsed["error"].is_null());
218        assert_eq!(parsed["result"]["pong"], true);
219    }
220
221    #[test]
222    fn test_router_unknown_method_returns_error() {
223        let usecases = create_test_usecases();
224        let router = Router::new(&usecases);
225
226        let request = RpcRequest::new(1, "nonexistent_method".to_string(), None);
227        let response = router.route(request);
228
229        let json_str = serde_json::to_string(&response).unwrap();
230        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
231
232        assert!(parsed.get("error").is_some());
233        assert_eq!(parsed["error"]["code"], -32601);
234        assert!(
235            parsed["error"]["message"]
236                .as_str()
237                .unwrap()
238                .contains("nonexistent_method")
239        );
240    }
241
242    #[test]
243    fn test_router_deprecated_screen_returns_error() {
244        let usecases = create_test_usecases();
245        let router = Router::new(&usecases);
246
247        let request = RpcRequest::new(1, "screen".to_string(), None);
248        let response = router.route(request);
249
250        let json_str = serde_json::to_string(&response).unwrap();
251        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
252
253        assert!(parsed.get("error").is_some());
254        assert_eq!(parsed["error"]["code"], -32601);
255        assert!(
256            parsed["error"]["message"]
257                .as_str()
258                .unwrap()
259                .contains("deprecated")
260        );
261    }
262
263    #[test]
264    fn test_router_health_returns_success() {
265        let usecases = create_test_usecases();
266        let router = Router::new(&usecases);
267
268        let request = RpcRequest::new(1, "health".to_string(), None);
269        let response = router.route(request);
270
271        let json_str = serde_json::to_string(&response).unwrap();
272        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
273
274        assert!(parsed.get("error").is_none() || parsed["error"].is_null());
275        assert!(parsed.get("result").is_some());
276        assert_eq!(parsed["result"]["status"], "healthy");
277    }
278
279    #[test]
280    fn test_router_sessions_returns_empty_list() {
281        let usecases = create_test_usecases();
282        let router = Router::new(&usecases);
283
284        let request = RpcRequest::new(1, "sessions".to_string(), None);
285        let response = router.route(request);
286
287        let json_str = serde_json::to_string(&response).unwrap();
288        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
289
290        assert!(parsed.get("error").is_none() || parsed["error"].is_null());
291        assert!(parsed["result"]["sessions"].is_array());
292    }
293
294    #[test]
295    fn test_router_cleanup_returns_success() {
296        let usecases = create_test_usecases();
297        let router = Router::new(&usecases);
298
299        let request = RpcRequest::new(1, "cleanup".to_string(), None);
300        let response = router.route(request);
301
302        let json_str = serde_json::to_string(&response).unwrap();
303        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
304
305        assert!(parsed.get("error").is_none() || parsed["error"].is_null());
306        assert!(parsed.get("result").is_some());
307        assert_eq!(parsed["result"]["sessions_cleaned"], 0);
308        assert_eq!(parsed["result"]["sessions_failed"], 0);
309        assert!(parsed["result"]["failures"].is_array());
310    }
311
312    #[test]
313    fn test_router_assert_invalid_condition_returns_error() {
314        let usecases = create_test_usecases();
315        let router = Router::new(&usecases);
316
317        // Invalid condition format (missing colon separator)
318        let request = RpcRequest::new(
319            1,
320            "assert".to_string(),
321            Some(json!({ "condition": "invalid" })),
322        );
323        let response = router.route(request);
324
325        let json_str = serde_json::to_string(&response).unwrap();
326        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
327
328        assert!(parsed.get("error").is_some());
329        assert_eq!(parsed["error"]["code"], -32602);
330        assert!(
331            parsed["error"]["message"]
332                .as_str()
333                .unwrap()
334                .contains("Invalid condition format")
335        );
336    }
337
338    #[test]
339    fn test_router_assert_session_condition_not_found() {
340        let usecases = create_test_usecases();
341        let router = Router::new(&usecases);
342
343        // Session condition for a non-existent session should return passed: false
344        let request = RpcRequest::new(
345            1,
346            "assert".to_string(),
347            Some(json!({ "condition": "session:nonexistent" })),
348        );
349        let response = router.route(request);
350
351        let json_str = serde_json::to_string(&response).unwrap();
352        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
353
354        assert!(parsed.get("error").is_none() || parsed["error"].is_null());
355        assert_eq!(parsed["result"]["passed"], false);
356        assert_eq!(parsed["result"]["condition"], "session:nonexistent");
357    }
358}