1use crate::ipc::{RpcRequest, RpcResponse, params};
2use serde_json::{Value, json};
3
4use super::recording_adapters::{build_asciicast, build_raw_frames};
5use super::snapshot_adapters::session_info_to_json;
6use crate::daemon::domain::{
7 AccessibilitySnapshotInput, AttachInput, AttachOutput, CleanupInput, CleanupOutput, ClearInput,
8 ClickInput, ConsoleInput, ConsoleOutput, CountInput, CountOutput, DomainElement,
9 DoubleClickInput, ElementStateInput, ErrorsInput, ErrorsOutput, FillInput, FindInput,
10 FindOutput, FocusCheckOutput, FocusInput, GetFocusedOutput, GetTextOutput, GetTitleOutput,
11 GetValueOutput, HealthOutput, IsCheckedOutput, IsEnabledOutput, KeydownInput, KeystrokeInput,
12 KeyupInput, KillOutput, MetricsOutput, MultiselectInput, MultiselectOutput, PtyReadInput,
13 PtyReadOutput, PtyWriteInput, PtyWriteOutput, RecordStartInput, RecordStartOutput,
14 RecordStatusInput, RecordStatusOutput, RecordStopInput, RecordStopOutput, ResizeInput,
15 ResizeOutput, RestartOutput, ScrollInput, ScrollIntoViewInput, ScrollIntoViewOutput,
16 ScrollOutput, SelectAllInput, SelectInput, SessionId, SessionInput, SessionsOutput,
17 SnapshotInput, SnapshotOutput, SpawnInput, SpawnOutput, ToggleInput, ToggleOutput, TraceInput,
18 TraceOutput, TypeInput, VisibilityOutput, WaitInput, WaitOutput,
19};
20use crate::daemon::error::{DomainError, SessionError};
21
22pub fn parse_session_id(session: Option<String>) -> Option<SessionId> {
29 session.and_then(|s| {
30 if s.trim().is_empty() {
31 None
32 } else {
33 Some(SessionId::new(s))
34 }
35 })
36}
37
38pub fn parse_session_input(request: &RpcRequest) -> SessionInput {
42 let session_id = parse_session_id(request.param_str("session").map(String::from));
43 SessionInput { session_id }
44}
45
46const MAX_TERMINAL_COLS: u16 = 500;
47const MAX_TERMINAL_ROWS: u16 = 200;
48const MIN_TERMINAL_COLS: u16 = 10;
49const MIN_TERMINAL_ROWS: u16 = 5;
50
51pub fn element_to_json(el: &DomainElement) -> Value {
53 json!({
54 "ref": el.element_ref,
55 "type": el.element_type.as_str(),
56 "label": el.label,
57 "value": el.value,
58 "position": {
59 "row": el.position.row,
60 "col": el.position.col,
61 "width": el.position.width,
62 "height": el.position.height
63 },
64 "focused": el.focused,
65 "selected": el.selected,
66 "checked": el.checked,
67 "disabled": el.disabled,
68 "hint": el.hint
69 })
70}
71
72pub fn domain_error_response(id: u64, err: &DomainError) -> RpcResponse {
74 RpcResponse::domain_error(
75 id,
76 err.code(),
77 &err.to_string(),
78 err.category().as_str(),
79 Some(err.context()),
80 Some(err.suggestion()),
81 )
82}
83
84pub fn session_error_response(id: u64, err: SessionError) -> RpcResponse {
86 domain_error_response(id, &DomainError::from(err))
87}
88
89pub fn lock_timeout_response(id: u64, session_id: Option<&str>) -> RpcResponse {
91 let err = DomainError::LockTimeout {
92 session_id: session_id.map(String::from),
93 };
94 domain_error_response(id, &err)
95}
96
97#[allow(clippy::result_large_err)]
99pub fn parse_spawn_input(request: &RpcRequest) -> Result<SpawnInput, RpcResponse> {
100 let rpc_params: params::SpawnParams = request
101 .params
102 .as_ref()
103 .ok_or_else(|| RpcResponse::error(request.id, -32602, "Missing params"))
104 .and_then(|p| {
105 serde_json::from_value(p.clone()).map_err(|e| {
106 RpcResponse::error(request.id, -32602, &format!("Invalid params: {}", e))
107 })
108 })?;
109
110 let command = if rpc_params.command.is_empty() {
112 "bash".to_string()
113 } else {
114 rpc_params.command
115 };
116
117 Ok(SpawnInput {
118 command,
119 args: rpc_params.args,
120 cwd: rpc_params.cwd,
121 env: None,
122 session_id: parse_session_id(rpc_params.session),
123 cols: rpc_params.cols.clamp(MIN_TERMINAL_COLS, MAX_TERMINAL_COLS),
124 rows: rpc_params.rows.clamp(MIN_TERMINAL_ROWS, MAX_TERMINAL_ROWS),
125 })
126}
127
128pub fn spawn_output_to_response(id: u64, output: SpawnOutput) -> RpcResponse {
130 RpcResponse::success(
131 id,
132 json!({
133 "session_id": output.session_id.as_str(),
134 "pid": output.pid
135 }),
136 )
137}
138
139pub fn parse_snapshot_input(request: &RpcRequest) -> SnapshotInput {
141 let rpc_params: params::SnapshotParams = request
143 .params
144 .as_ref()
145 .and_then(|p| serde_json::from_value(p.clone()).ok())
146 .unwrap_or_default();
147
148 SnapshotInput {
149 session_id: parse_session_id(rpc_params.session),
150 include_elements: rpc_params.include_elements,
151 region: rpc_params.region,
152 strip_ansi: rpc_params.strip_ansi,
153 include_cursor: rpc_params.include_cursor,
154 }
155}
156
157pub fn snapshot_output_to_response(
161 id: u64,
162 output: SnapshotOutput,
163 strip_ansi: bool,
164) -> RpcResponse {
165 use crate::common::strip_ansi_codes;
166
167 let screen = if strip_ansi {
168 strip_ansi_codes(&output.screen)
169 } else {
170 output.screen
171 };
172
173 let mut result = json!({
174 "session_id": output.session_id.as_str(),
175 "screen": screen
176 });
177
178 if let Some(elements) = output.elements {
179 result["elements"] = json!(elements.iter().map(element_to_json).collect::<Vec<_>>());
180 }
181
182 if let Some(cursor) = output.cursor {
183 result["cursor"] = json!({
184 "row": cursor.row,
185 "col": cursor.col,
186 "visible": cursor.visible
187 });
188 }
189
190 RpcResponse::success(id, result)
191}
192
193#[allow(clippy::result_large_err)]
195pub fn parse_click_input(request: &RpcRequest) -> Result<ClickInput, RpcResponse> {
196 let element_ref = request.require_str("ref")?.to_string();
197
198 Ok(ClickInput {
199 session_id: parse_session_id(request.param_str("session").map(String::from)),
200 element_ref,
201 })
202}
203
204#[allow(clippy::result_large_err)]
206pub fn parse_fill_input(request: &RpcRequest) -> Result<FillInput, RpcResponse> {
207 let element_ref = request.require_str("ref")?.to_string();
208 let value = request.require_str("value")?.to_string();
209
210 Ok(FillInput {
211 session_id: parse_session_id(request.param_str("session").map(String::from)),
212 element_ref,
213 value,
214 })
215}
216
217pub fn parse_find_input(request: &RpcRequest) -> FindInput {
219 let rpc_params: params::FindParams = request
220 .params
221 .as_ref()
222 .and_then(|p| serde_json::from_value(p.clone()).ok())
223 .unwrap_or_default();
224
225 FindInput {
226 session_id: parse_session_id(rpc_params.session),
227 role: rpc_params.role,
228 name: rpc_params.name,
229 text: rpc_params.text,
230 placeholder: rpc_params.placeholder,
231 focused: rpc_params.focused,
232 nth: rpc_params.nth,
233 exact: rpc_params.exact,
234 }
235}
236
237#[allow(clippy::result_large_err)]
239pub fn parse_keystroke_input(request: &RpcRequest) -> Result<KeystrokeInput, RpcResponse> {
240 let key = request.require_str("key")?.to_string();
241
242 Ok(KeystrokeInput {
243 session_id: parse_session_id(request.param_str("session").map(String::from)),
244 key,
245 })
246}
247
248#[allow(clippy::result_large_err)]
250pub fn parse_type_input(request: &RpcRequest) -> Result<TypeInput, RpcResponse> {
251 let text = request.require_str("text")?.to_string();
252
253 Ok(TypeInput {
254 session_id: parse_session_id(request.param_str("session").map(String::from)),
255 text,
256 })
257}
258
259pub fn parse_wait_input(request: &RpcRequest) -> WaitInput {
261 let rpc_params: params::WaitParams = request
262 .params
263 .as_ref()
264 .and_then(|p| serde_json::from_value(p.clone()).ok())
265 .unwrap_or_default();
266
267 WaitInput {
268 session_id: parse_session_id(rpc_params.session),
269 text: rpc_params.text,
270 timeout_ms: rpc_params.timeout_ms,
271 condition: rpc_params.condition,
272 target: rpc_params.target,
273 }
274}
275
276pub fn wait_output_to_response(id: u64, output: WaitOutput) -> RpcResponse {
278 RpcResponse::success(
279 id,
280 json!({
281 "found": output.found,
282 "elapsed_ms": output.elapsed_ms
283 }),
284 )
285}
286
287#[allow(clippy::result_large_err)]
289pub fn parse_scroll_input(request: &RpcRequest) -> Result<ScrollInput, RpcResponse> {
290 let direction = request.require_str("direction")?.to_string();
291
292 Ok(ScrollInput {
293 session_id: parse_session_id(request.param_str("session").map(String::from)),
294 direction,
295 amount: request.param_u16("amount", 1),
296 })
297}
298
299pub fn kill_output_to_response(id: u64, output: KillOutput) -> RpcResponse {
301 RpcResponse::success(
302 id,
303 json!({
304 "success": output.success,
305 "session_id": output.session_id.as_str()
306 }),
307 )
308}
309
310pub fn sessions_output_to_response(id: u64, output: SessionsOutput) -> RpcResponse {
312 RpcResponse::success(
313 id,
314 json!({
315 "sessions": output.sessions.iter().map(session_info_to_json).collect::<Vec<_>>(),
316 "active_session": output.active_session.as_ref().map(|id| id.as_str())
317 }),
318 )
319}
320
321pub fn success_response(id: u64, message: &str) -> RpcResponse {
323 RpcResponse::success(
324 id,
325 json!({
326 "success": true,
327 "message": message
328 }),
329 )
330}
331
332pub fn click_success_response(id: u64, element_ref: &str, warning: Option<&str>) -> RpcResponse {
334 let mut result = json!({
335 "success": true,
336 "message": format!("Clicked {}", element_ref)
337 });
338 if let Some(w) = warning {
339 result["warning"] = json!(w);
340 }
341 RpcResponse::success(id, result)
342}
343
344pub fn fill_success_response(id: u64, element_ref: &str) -> RpcResponse {
346 RpcResponse::success(
347 id,
348 json!({
349 "success": true,
350 "message": format!("Filled {} with value", element_ref)
351 }),
352 )
353}
354
355pub fn parse_resize_input(request: &RpcRequest) -> ResizeInput {
357 let rpc_params: params::ResizeParams = request
358 .params
359 .as_ref()
360 .and_then(|p| serde_json::from_value(p.clone()).ok())
361 .unwrap_or(params::ResizeParams {
362 cols: 80,
363 rows: 24,
364 session: None,
365 });
366
367 ResizeInput {
368 session_id: parse_session_id(rpc_params.session),
369 cols: rpc_params.cols.clamp(MIN_TERMINAL_COLS, MAX_TERMINAL_COLS),
370 rows: rpc_params.rows.clamp(MIN_TERMINAL_ROWS, MAX_TERMINAL_ROWS),
371 }
372}
373
374pub fn resize_output_to_response(id: u64, output: ResizeOutput) -> RpcResponse {
376 RpcResponse::success(
377 id,
378 json!({
379 "success": output.success,
380 "session_id": output.session_id.as_str(),
381 "size": { "cols": output.cols, "rows": output.rows }
382 }),
383 )
384}
385
386pub fn restart_output_to_response(id: u64, output: RestartOutput) -> RpcResponse {
388 RpcResponse::success(
389 id,
390 json!({
391 "success": true,
392 "old_session_id": output.old_session_id.as_str(),
393 "new_session_id": output.new_session_id.as_str(),
394 "command": output.command,
395 "pid": output.pid
396 }),
397 )
398}
399
400#[allow(clippy::result_large_err)]
402pub fn parse_attach_input(request: &RpcRequest) -> Result<AttachInput, RpcResponse> {
403 let session_id = request.require_str("session")?;
404 Ok(AttachInput {
405 session_id: SessionId::new(session_id),
406 })
407}
408
409pub fn attach_output_to_response(id: u64, output: AttachOutput) -> RpcResponse {
411 RpcResponse::success(
412 id,
413 json!({
414 "success": output.success,
415 "session_id": output.session_id.as_str(),
416 "message": output.message
417 }),
418 )
419}
420
421pub fn parse_cleanup_input(request: &RpcRequest) -> CleanupInput {
423 let all = request.param_bool("all", false);
424 CleanupInput { all }
425}
426
427pub fn cleanup_output_to_response(id: u64, output: CleanupOutput) -> RpcResponse {
429 let failures_json: Vec<Value> = output
430 .failures
431 .iter()
432 .map(|f| {
433 json!({
434 "session": f.session_id.as_str(),
435 "error": f.error
436 })
437 })
438 .collect();
439
440 RpcResponse::success(
441 id,
442 json!({
443 "sessions_cleaned": output.cleaned,
444 "sessions_failed": output.failures.len(),
445 "failures": failures_json
446 }),
447 )
448}
449
450use crate::daemon::domain::{AssertConditionType, AssertInput, AssertOutput};
451
452#[allow(clippy::result_large_err)]
456pub fn parse_assert_input(request: &RpcRequest) -> Result<AssertInput, RpcResponse> {
457 let condition = request.param_str("condition").unwrap_or("");
458 let session = request.param_str("session").map(String::from);
459
460 let parts: Vec<&str> = condition.splitn(2, ':').collect();
461 if parts.len() != 2 {
462 return Err(RpcResponse::error(
463 request.id,
464 -32602,
465 "Invalid condition format. Use: text:pattern, element:ref, or session:id",
466 ));
467 }
468
469 let (cond_type_str, value) = (parts[0], parts[1]);
470
471 let condition_type = match AssertConditionType::parse(cond_type_str) {
472 Ok(ct) => ct,
473 Err(msg) => {
474 return Err(RpcResponse::error(request.id, -32602, &msg));
475 }
476 };
477
478 Ok(AssertInput {
479 session_id: parse_session_id(session),
480 condition_type,
481 value: value.to_string(),
482 })
483}
484
485pub fn assert_output_to_response(id: u64, output: AssertOutput) -> RpcResponse {
487 RpcResponse::success(
488 id,
489 json!({
490 "condition": output.condition,
491 "passed": output.passed
492 }),
493 )
494}
495
496pub fn scroll_output_to_response(
498 id: u64,
499 output: ScrollOutput,
500 direction: &str,
501 amount: u16,
502) -> RpcResponse {
503 RpcResponse::success(
504 id,
505 json!({
506 "success": output.success,
507 "direction": direction,
508 "amount": amount
509 }),
510 )
511}
512
513pub fn parse_count_input(request: &RpcRequest) -> CountInput {
515 let rpc_params: params::CountParams = request
516 .params
517 .as_ref()
518 .and_then(|p| serde_json::from_value(p.clone()).ok())
519 .unwrap_or_default();
520
521 CountInput {
522 session_id: parse_session_id(rpc_params.session),
523 role: rpc_params.role,
524 name: rpc_params.name,
525 text: rpc_params.text,
526 }
527}
528
529pub fn count_output_to_response(id: u64, output: CountOutput) -> RpcResponse {
531 RpcResponse::success(id, json!({ "count": output.count }))
532}
533
534pub fn health_output_to_response(id: u64, output: HealthOutput) -> RpcResponse {
540 RpcResponse::success(
541 id,
542 json!({
543 "status": output.status,
544 "pid": output.pid,
545 "uptime_ms": output.uptime_ms,
546 "session_count": output.session_count,
547 "version": output.version,
548 "active_connections": output.active_connections,
549 "total_requests": output.total_requests,
550 "error_count": output.error_count
551 }),
552 )
553}
554
555pub fn metrics_output_to_response(id: u64, output: MetricsOutput) -> RpcResponse {
557 RpcResponse::success(
558 id,
559 json!({
560 "requests_total": output.requests_total,
561 "errors_total": output.errors_total,
562 "lock_timeouts": output.lock_timeouts,
563 "poison_recoveries": output.poison_recoveries,
564 "uptime_ms": output.uptime_ms,
565 "active_connections": output.active_connections,
566 "session_count": output.session_count
567 }),
568 )
569}
570
571pub fn trace_output_to_response(id: u64, output: TraceOutput) -> RpcResponse {
573 let trace_json: Vec<_> = output
574 .entries
575 .iter()
576 .map(|t| {
577 json!({
578 "timestamp_ms": t.timestamp_ms,
579 "action": t.action,
580 "details": t.details
581 })
582 })
583 .collect();
584
585 RpcResponse::success(
586 id,
587 json!({
588 "trace": trace_json,
589 "count": trace_json.len()
590 }),
591 )
592}
593
594pub fn console_output_to_response(id: u64, output: ConsoleOutput) -> RpcResponse {
596 RpcResponse::success(
597 id,
598 json!({
599 "output": output.lines,
600 "line_count": output.lines.len()
601 }),
602 )
603}
604
605pub fn errors_output_to_response(id: u64, output: ErrorsOutput) -> RpcResponse {
607 let errors_json: Vec<_> = output
608 .errors
609 .iter()
610 .map(|e| {
611 json!({
612 "timestamp": e.timestamp,
613 "message": e.message,
614 "source": e.source
615 })
616 })
617 .collect();
618
619 RpcResponse::success(
620 id,
621 json!({
622 "errors": errors_json,
623 "count": errors_json.len()
624 }),
625 )
626}
627
628pub fn pty_read_output_to_response(id: u64, output: PtyReadOutput) -> RpcResponse {
630 RpcResponse::success(
631 id,
632 json!({
633 "session_id": output.session_id.as_str(),
634 "data": output.data,
635 "bytes_read": output.bytes_read
636 }),
637 )
638}
639
640pub fn pty_write_output_to_response(id: u64, output: PtyWriteOutput) -> RpcResponse {
642 RpcResponse::success(
643 id,
644 json!({
645 "session_id": output.session_id.as_str(),
646 "bytes_written": output.bytes_written,
647 "success": output.success
648 }),
649 )
650}
651
652pub fn record_start_output_to_response(id: u64, output: RecordStartOutput) -> RpcResponse {
658 RpcResponse::success(
659 id,
660 json!({
661 "success": output.success,
662 "session_id": output.session_id.as_str(),
663 "recording": true
664 }),
665 )
666}
667
668pub fn record_status_output_to_response(id: u64, output: RecordStatusOutput) -> RpcResponse {
670 RpcResponse::success(
671 id,
672 json!({
673 "recording": output.is_recording,
674 "frame_count": output.frame_count,
675 "duration_ms": output.duration_ms
676 }),
677 )
678}
679
680pub fn record_stop_output_to_response(id: u64, output: RecordStopOutput) -> RpcResponse {
682 let recording_data = if output.format == "asciicast" {
683 build_asciicast(
684 output.session_id.as_ref(),
685 output.cols,
686 output.rows,
687 &output.frames,
688 )
689 } else {
690 build_raw_frames(&output.frames)
691 };
692
693 RpcResponse::success(
694 id,
695 json!({
696 "success": true,
697 "session_id": output.session_id.as_str(),
698 "frame_count": output.frame_count,
699 "recording": recording_data
700 }),
701 )
702}
703
704pub fn find_output_to_response(id: u64, output: FindOutput) -> RpcResponse {
710 let elements_json: Vec<Value> = output.elements.iter().map(element_to_json).collect();
711 RpcResponse::success(
712 id,
713 json!({
714 "elements": elements_json,
715 "count": output.count
716 }),
717 )
718}
719
720pub fn get_text_output_to_response(
722 id: u64,
723 element_ref: &str,
724 output: GetTextOutput,
725) -> RpcResponse {
726 RpcResponse::success(
727 id,
728 json!({ "ref": element_ref, "text": output.text, "found": output.found }),
729 )
730}
731
732pub fn get_value_output_to_response(
734 id: u64,
735 element_ref: &str,
736 output: GetValueOutput,
737) -> RpcResponse {
738 RpcResponse::success(
739 id,
740 json!({ "ref": element_ref, "value": output.value, "found": output.found }),
741 )
742}
743
744pub fn visibility_output_to_response(
746 id: u64,
747 element_ref: &str,
748 output: VisibilityOutput,
749) -> RpcResponse {
750 RpcResponse::success(id, json!({ "ref": element_ref, "visible": output.visible }))
751}
752
753pub fn focus_check_output_to_response(
755 id: u64,
756 element_ref: &str,
757 output: FocusCheckOutput,
758) -> RpcResponse {
759 RpcResponse::success(
760 id,
761 json!({ "ref": element_ref, "focused": output.focused, "found": output.found }),
762 )
763}
764
765pub fn is_enabled_output_to_response(
767 id: u64,
768 element_ref: &str,
769 output: IsEnabledOutput,
770) -> RpcResponse {
771 RpcResponse::success(
772 id,
773 json!({ "ref": element_ref, "enabled": output.enabled, "found": output.found }),
774 )
775}
776
777pub fn is_checked_output_to_response(
779 id: u64,
780 element_ref: &str,
781 output: IsCheckedOutput,
782) -> RpcResponse {
783 let mut response =
784 json!({ "ref": element_ref, "checked": output.checked, "found": output.found });
785 if let Some(msg) = output.message {
786 response["message"] = json!(msg);
787 }
788 RpcResponse::success(id, response)
789}
790
791pub fn get_focused_output_to_response(id: u64, output: GetFocusedOutput) -> RpcResponse {
793 if let Some(el) = output.element {
794 RpcResponse::success(
795 id,
796 json!({
797 "ref": el.element_ref,
798 "type": el.element_type.as_str(),
799 "label": el.label,
800 "value": el.value,
801 "found": true
802 }),
803 )
804 } else {
805 RpcResponse::success(
806 id,
807 json!({
808 "found": false,
809 "message": "No focused element found."
810 }),
811 )
812 }
813}
814
815pub fn get_title_output_to_response(id: u64, output: GetTitleOutput) -> RpcResponse {
817 RpcResponse::success(
818 id,
819 json!({
820 "session_id": output.session_id.as_str(),
821 "title": output.title
822 }),
823 )
824}
825
826pub fn toggle_output_to_response(id: u64, element_ref: &str, output: ToggleOutput) -> RpcResponse {
828 RpcResponse::success(
829 id,
830 json!({ "success": true, "ref": element_ref, "checked": output.checked }),
831 )
832}
833
834pub fn select_output_to_response(id: u64, element_ref: &str, option: &str) -> RpcResponse {
836 RpcResponse::success(
837 id,
838 json!({ "success": true, "ref": element_ref, "option": option }),
839 )
840}
841
842pub fn scroll_into_view_output_to_response(
844 id: u64,
845 element_ref: &str,
846 output: ScrollIntoViewOutput,
847) -> RpcResponse {
848 if output.success {
849 RpcResponse::success(
850 id,
851 json!({
852 "success": true,
853 "ref": element_ref,
854 "scrolls_needed": output.scrolls_needed
855 }),
856 )
857 } else {
858 RpcResponse::success(
859 id,
860 json!({
861 "success": false,
862 "message": output.message.unwrap_or_else(|| "Element not found after scrolling".to_string())
863 }),
864 )
865 }
866}
867
868pub fn multiselect_output_to_response(
870 id: u64,
871 element_ref: &str,
872 output: MultiselectOutput,
873) -> RpcResponse {
874 RpcResponse::success(
875 id,
876 json!({ "success": true, "ref": element_ref, "selected_options": output.selected_options }),
877 )
878}
879
880#[allow(clippy::result_large_err)]
886pub fn parse_double_click_input(request: &RpcRequest) -> Result<DoubleClickInput, RpcResponse> {
887 let element_ref = request.require_str("ref")?.to_string();
888
889 Ok(DoubleClickInput {
890 session_id: parse_session_id(request.param_str("session").map(String::from)),
891 element_ref,
892 })
893}
894
895#[allow(clippy::result_large_err)]
897pub fn parse_focus_input(request: &RpcRequest) -> Result<FocusInput, RpcResponse> {
898 let element_ref = request.require_str("ref")?.to_string();
899
900 Ok(FocusInput {
901 session_id: parse_session_id(request.param_str("session").map(String::from)),
902 element_ref,
903 })
904}
905
906#[allow(clippy::result_large_err)]
908pub fn parse_clear_input(request: &RpcRequest) -> Result<ClearInput, RpcResponse> {
909 let element_ref = request.require_str("ref")?.to_string();
910
911 Ok(ClearInput {
912 session_id: parse_session_id(request.param_str("session").map(String::from)),
913 element_ref,
914 })
915}
916
917#[allow(clippy::result_large_err)]
919pub fn parse_select_all_input(request: &RpcRequest) -> Result<SelectAllInput, RpcResponse> {
920 let element_ref = request.require_str("ref")?.to_string();
921
922 Ok(SelectAllInput {
923 session_id: parse_session_id(request.param_str("session").map(String::from)),
924 element_ref,
925 })
926}
927
928#[allow(clippy::result_large_err)]
930pub fn parse_scroll_into_view_input(
931 request: &RpcRequest,
932) -> Result<ScrollIntoViewInput, RpcResponse> {
933 let element_ref = request.require_str("ref")?.to_string();
934
935 Ok(ScrollIntoViewInput {
936 session_id: parse_session_id(request.param_str("session").map(String::from)),
937 element_ref,
938 })
939}
940
941#[allow(clippy::result_large_err)]
943pub fn parse_element_state_input(request: &RpcRequest) -> Result<ElementStateInput, RpcResponse> {
944 let element_ref = request.require_str("ref")?.to_string();
945
946 Ok(ElementStateInput {
947 session_id: parse_session_id(request.param_str("session").map(String::from)),
948 element_ref,
949 })
950}
951
952#[allow(clippy::result_large_err)]
954pub fn parse_toggle_input(request: &RpcRequest) -> Result<ToggleInput, RpcResponse> {
955 let element_ref = request.require_str("ref")?.to_string();
956
957 Ok(ToggleInput {
958 session_id: parse_session_id(request.param_str("session").map(String::from)),
959 element_ref,
960 state: request.param_bool_opt("state"),
961 })
962}
963
964#[allow(clippy::result_large_err)]
966pub fn parse_select_input(request: &RpcRequest) -> Result<SelectInput, RpcResponse> {
967 let element_ref = request.require_str("ref")?.to_string();
968 let option = request.require_str("option")?.to_string();
969
970 Ok(SelectInput {
971 session_id: parse_session_id(request.param_str("session").map(String::from)),
972 element_ref,
973 option,
974 })
975}
976
977#[allow(clippy::result_large_err)]
979pub fn parse_multiselect_input(request: &RpcRequest) -> Result<MultiselectInput, RpcResponse> {
980 let options: Vec<String> = request
981 .require_array("options")?
982 .iter()
983 .filter_map(|v| v.as_str().map(str::to_owned))
984 .collect();
985
986 if options.is_empty() {
987 return Err(RpcResponse::error(
988 request.id,
989 -32602,
990 "Options array cannot be empty",
991 ));
992 }
993
994 let element_ref = request.require_str("ref")?.to_string();
995
996 Ok(MultiselectInput {
997 session_id: parse_session_id(request.param_str("session").map(String::from)),
998 element_ref,
999 options,
1000 })
1001}
1002
1003#[allow(clippy::result_large_err)]
1009pub fn parse_keydown_input(request: &RpcRequest) -> Result<KeydownInput, RpcResponse> {
1010 let key = request.require_str("key")?.to_string();
1011
1012 Ok(KeydownInput {
1013 session_id: parse_session_id(request.param_str("session").map(String::from)),
1014 key,
1015 })
1016}
1017
1018#[allow(clippy::result_large_err)]
1020pub fn parse_keyup_input(request: &RpcRequest) -> Result<KeyupInput, RpcResponse> {
1021 let key = request.require_str("key")?.to_string();
1022
1023 Ok(KeyupInput {
1024 session_id: parse_session_id(request.param_str("session").map(String::from)),
1025 key,
1026 })
1027}
1028
1029pub fn parse_record_start_input(request: &RpcRequest) -> RecordStartInput {
1035 RecordStartInput {
1036 session_id: parse_session_id(request.param_str("session").map(String::from)),
1037 }
1038}
1039
1040pub fn parse_record_stop_input(request: &RpcRequest) -> RecordStopInput {
1042 RecordStopInput {
1043 session_id: parse_session_id(request.param_str("session").map(String::from)),
1044 format: request.param_str("format").map(String::from),
1045 }
1046}
1047
1048pub fn parse_record_status_input(request: &RpcRequest) -> RecordStatusInput {
1050 RecordStatusInput {
1051 session_id: parse_session_id(request.param_str("session").map(String::from)),
1052 }
1053}
1054
1055pub fn parse_accessibility_snapshot_input(request: &RpcRequest) -> AccessibilitySnapshotInput {
1061 AccessibilitySnapshotInput {
1062 session_id: parse_session_id(request.param_str("session").map(String::from)),
1063 interactive_only: request.param_bool("interactive", false),
1064 }
1065}
1066
1067pub fn parse_trace_input(request: &RpcRequest) -> TraceInput {
1069 TraceInput {
1070 session_id: parse_session_id(request.param_str("session").map(String::from)),
1071 start: false,
1072 stop: false,
1073 count: request.param_u64("count", 1000) as usize,
1074 }
1075}
1076
1077pub fn parse_console_input(request: &RpcRequest) -> ConsoleInput {
1079 ConsoleInput {
1080 session_id: parse_session_id(request.param_str("session").map(String::from)),
1081 count: 0,
1082 clear: false,
1083 }
1084}
1085
1086pub fn parse_errors_input(request: &RpcRequest) -> ErrorsInput {
1088 ErrorsInput {
1089 session_id: parse_session_id(request.param_str("session").map(String::from)),
1090 count: request.param_u64("count", 1000) as usize,
1091 clear: false,
1092 }
1093}
1094
1095pub fn parse_pty_read_input(request: &RpcRequest) -> PtyReadInput {
1097 PtyReadInput {
1098 session_id: parse_session_id(request.param_str("session").map(String::from)),
1099 max_bytes: request.param_u64("max_bytes", 4096) as usize,
1100 }
1101}
1102
1103#[allow(clippy::result_large_err)]
1105pub fn parse_pty_write_input(request: &RpcRequest) -> Result<PtyWriteInput, RpcResponse> {
1106 let data = request.require_str("data")?.to_string();
1107
1108 Ok(PtyWriteInput {
1109 session_id: parse_session_id(request.param_str("session").map(String::from)),
1110 data,
1111 })
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117 use crate::daemon::domain::SessionId;
1118
1119 fn make_request(id: u64, method: &str, params: Option<serde_json::Value>) -> RpcRequest {
1120 RpcRequest::new(id, method.to_string(), params)
1121 }
1122
1123 #[test]
1124 fn test_parse_spawn_input_defaults() {
1125 let request = make_request(1, "spawn", Some(json!({})));
1126 let input = parse_spawn_input(&request).unwrap();
1127 assert_eq!(input.command, "bash");
1128 assert!(input.args.is_empty());
1129 assert_eq!(input.cols, 80);
1130 assert_eq!(input.rows, 24);
1131 }
1132
1133 #[test]
1134 fn test_parse_spawn_input_custom() {
1135 let request = make_request(
1136 1,
1137 "spawn",
1138 Some(json!({
1139 "command": "vim",
1140 "args": ["file.txt"],
1141 "cols": 120,
1142 "rows": 40,
1143 "cwd": "/home/user"
1144 })),
1145 );
1146 let input = parse_spawn_input(&request).unwrap();
1147 assert_eq!(input.command, "vim");
1148 assert_eq!(input.args, vec!["file.txt"]);
1149 assert_eq!(input.cols, 120);
1150 assert_eq!(input.rows, 40);
1151 assert_eq!(input.cwd, Some("/home/user".to_string()));
1152 }
1153
1154 #[test]
1155 fn test_parse_spawn_input_clamps_values() {
1156 let request = make_request(
1157 1,
1158 "spawn",
1159 Some(json!({
1160 "cols": 1000,
1161 "rows": 500
1162 })),
1163 );
1164 let input = parse_spawn_input(&request).unwrap();
1165 assert_eq!(input.cols, MAX_TERMINAL_COLS);
1166 assert_eq!(input.rows, MAX_TERMINAL_ROWS);
1167 }
1168
1169 #[test]
1170 fn test_parse_snapshot_input() {
1171 let request = make_request(
1172 1,
1173 "snapshot",
1174 Some(json!({
1175 "session": "abc123",
1176 "include_elements": true,
1177 "include_cursor": true
1178 })),
1179 );
1180 let input = parse_snapshot_input(&request);
1181 assert_eq!(input.session_id, Some(SessionId::new("abc123")));
1182 assert!(input.include_elements);
1183 assert!(input.include_cursor);
1184 }
1185
1186 #[test]
1187 fn test_parse_wait_input() {
1188 let request = make_request(
1189 1,
1190 "wait",
1191 Some(json!({
1192 "text": "Ready",
1193 "timeout_ms": 5000
1194 })),
1195 );
1196 let input = parse_wait_input(&request);
1197 assert_eq!(input.text, Some("Ready".to_string()));
1198 assert_eq!(input.timeout_ms, 5000);
1199 }
1200
1201 #[test]
1206 fn test_parse_double_click_input() {
1207 let request = make_request(
1208 1,
1209 "dbl_click",
1210 Some(json!({ "ref": "@btn1", "session": "sess1" })),
1211 );
1212 let input = parse_double_click_input(&request).unwrap();
1213 assert_eq!(input.element_ref, "@btn1");
1214 assert_eq!(input.session_id, Some(SessionId::new("sess1")));
1215 }
1216
1217 #[test]
1218 fn test_parse_double_click_input_missing_ref() {
1219 let request = make_request(1, "dbl_click", Some(json!({})));
1220 let result = parse_double_click_input(&request);
1221 assert!(result.is_err());
1222 }
1223
1224 #[test]
1225 fn test_parse_focus_input() {
1226 let request = make_request(1, "focus", Some(json!({ "ref": "@input1" })));
1227 let input = parse_focus_input(&request).unwrap();
1228 assert_eq!(input.element_ref, "@input1");
1229 assert!(input.session_id.is_none());
1230 }
1231
1232 #[test]
1233 fn test_parse_clear_input() {
1234 let request = make_request(1, "clear", Some(json!({ "ref": "@field" })));
1235 let input = parse_clear_input(&request).unwrap();
1236 assert_eq!(input.element_ref, "@field");
1237 }
1238
1239 #[test]
1240 fn test_parse_select_all_input() {
1241 let request = make_request(1, "select_all", Some(json!({ "ref": "@textarea" })));
1242 let input = parse_select_all_input(&request).unwrap();
1243 assert_eq!(input.element_ref, "@textarea");
1244 }
1245
1246 #[test]
1247 fn test_parse_scroll_into_view_input() {
1248 let request = make_request(1, "scroll_into_view", Some(json!({ "ref": "@item" })));
1249 let input = parse_scroll_into_view_input(&request).unwrap();
1250 assert_eq!(input.element_ref, "@item");
1251 }
1252
1253 #[test]
1254 fn test_parse_element_state_input() {
1255 let request = make_request(1, "get_text", Some(json!({ "ref": "@label" })));
1256 let input = parse_element_state_input(&request).unwrap();
1257 assert_eq!(input.element_ref, "@label");
1258 }
1259
1260 #[test]
1261 fn test_parse_toggle_input_with_state() {
1262 let request = make_request(
1263 1,
1264 "toggle",
1265 Some(json!({ "ref": "@checkbox", "state": true })),
1266 );
1267 let input = parse_toggle_input(&request).unwrap();
1268 assert_eq!(input.element_ref, "@checkbox");
1269 assert_eq!(input.state, Some(true));
1270 }
1271
1272 #[test]
1273 fn test_parse_toggle_input_without_state() {
1274 let request = make_request(1, "toggle", Some(json!({ "ref": "@checkbox" })));
1275 let input = parse_toggle_input(&request).unwrap();
1276 assert!(input.state.is_none());
1277 }
1278
1279 #[test]
1280 fn test_parse_select_input() {
1281 let request = make_request(
1282 1,
1283 "select",
1284 Some(json!({ "ref": "@dropdown", "option": "choice1" })),
1285 );
1286 let input = parse_select_input(&request).unwrap();
1287 assert_eq!(input.element_ref, "@dropdown");
1288 assert_eq!(input.option, "choice1");
1289 }
1290
1291 #[test]
1292 fn test_parse_select_input_missing_option() {
1293 let request = make_request(1, "select", Some(json!({ "ref": "@dropdown" })));
1294 let result = parse_select_input(&request);
1295 assert!(result.is_err());
1296 }
1297
1298 #[test]
1299 fn test_parse_multiselect_input() {
1300 let request = make_request(
1301 1,
1302 "multiselect",
1303 Some(json!({ "ref": "@list", "options": ["a", "b", "c"] })),
1304 );
1305 let input = parse_multiselect_input(&request).unwrap();
1306 assert_eq!(input.element_ref, "@list");
1307 assert_eq!(input.options, vec!["a", "b", "c"]);
1308 }
1309
1310 #[test]
1311 fn test_parse_multiselect_input_empty_options() {
1312 let request = make_request(
1313 1,
1314 "multiselect",
1315 Some(json!({ "ref": "@list", "options": [] })),
1316 );
1317 let result = parse_multiselect_input(&request);
1318 assert!(result.is_err());
1319 }
1320
1321 #[test]
1326 fn test_parse_keydown_input() {
1327 let request = make_request(1, "keydown", Some(json!({ "key": "Shift" })));
1328 let input = parse_keydown_input(&request).unwrap();
1329 assert_eq!(input.key, "Shift");
1330 }
1331
1332 #[test]
1333 fn test_parse_keyup_input() {
1334 let request = make_request(1, "keyup", Some(json!({ "key": "Control" })));
1335 let input = parse_keyup_input(&request).unwrap();
1336 assert_eq!(input.key, "Control");
1337 }
1338
1339 #[test]
1344 fn test_parse_record_start_input() {
1345 let request = make_request(1, "record_start", Some(json!({ "session": "rec-session" })));
1346 let input = parse_record_start_input(&request);
1347 assert_eq!(input.session_id, Some(SessionId::new("rec-session")));
1348 }
1349
1350 #[test]
1351 fn test_parse_record_stop_input() {
1352 let request = make_request(1, "record_stop", Some(json!({ "format": "asciicast" })));
1353 let input = parse_record_stop_input(&request);
1354 assert_eq!(input.format, Some("asciicast".to_string()));
1355 }
1356
1357 #[test]
1358 fn test_parse_record_status_input() {
1359 let request = make_request(1, "record_status", None);
1360 let input = parse_record_status_input(&request);
1361 assert!(input.session_id.is_none());
1362 }
1363
1364 #[test]
1369 fn test_parse_accessibility_snapshot_input() {
1370 let request = make_request(
1371 1,
1372 "accessibility_snapshot",
1373 Some(json!({ "interactive": true })),
1374 );
1375 let input = parse_accessibility_snapshot_input(&request);
1376 assert!(input.interactive_only);
1377 }
1378
1379 #[test]
1380 fn test_parse_trace_input() {
1381 let request = make_request(1, "trace", Some(json!({ "count": 500 })));
1382 let input = parse_trace_input(&request);
1383 assert_eq!(input.count, 500);
1384 }
1385
1386 #[test]
1387 fn test_parse_trace_input_defaults() {
1388 let request = make_request(1, "trace", None);
1389 let input = parse_trace_input(&request);
1390 assert_eq!(input.count, 1000);
1391 }
1392
1393 #[test]
1394 fn test_parse_console_input() {
1395 let request = make_request(1, "console", None);
1396 let input = parse_console_input(&request);
1397 assert_eq!(input.count, 0);
1398 assert!(!input.clear);
1399 }
1400
1401 #[test]
1402 fn test_parse_errors_input() {
1403 let request = make_request(1, "errors", Some(json!({ "count": 50 })));
1404 let input = parse_errors_input(&request);
1405 assert_eq!(input.count, 50);
1406 }
1407
1408 #[test]
1409 fn test_parse_pty_read_input() {
1410 let request = make_request(1, "pty_read", Some(json!({ "max_bytes": 8192 })));
1411 let input = parse_pty_read_input(&request);
1412 assert_eq!(input.max_bytes, 8192);
1413 }
1414
1415 #[test]
1416 fn test_parse_pty_read_input_defaults() {
1417 let request = make_request(1, "pty_read", None);
1418 let input = parse_pty_read_input(&request);
1419 assert_eq!(input.max_bytes, 4096);
1420 }
1421
1422 #[test]
1423 fn test_parse_pty_write_input() {
1424 let request = make_request(1, "pty_write", Some(json!({ "data": "hello" })));
1425 let input = parse_pty_write_input(&request).unwrap();
1426 assert_eq!(input.data, "hello");
1427 }
1428
1429 #[test]
1430 fn test_parse_pty_write_input_missing_data() {
1431 let request = make_request(1, "pty_write", Some(json!({})));
1432 let result = parse_pty_write_input(&request);
1433 assert!(result.is_err());
1434 }
1435
1436 fn extract_result(response: RpcResponse) -> serde_json::Value {
1441 let json_str = serde_json::to_string(&response).expect("serialize");
1442 let parsed: serde_json::Value = serde_json::from_str(&json_str).expect("parse");
1443 parsed["result"].clone()
1444 }
1445
1446 #[test]
1447 fn test_health_output_to_response() {
1448 let output = HealthOutput {
1449 status: "healthy".to_string(),
1450 pid: 1234,
1451 uptime_ms: 5000,
1452 session_count: 2,
1453 version: "0.1.0".to_string(),
1454 active_connections: 1,
1455 total_requests: 100,
1456 error_count: 5,
1457 };
1458 let response = health_output_to_response(1, output);
1459 let result = extract_result(response);
1460 assert_eq!(result["status"], "healthy");
1461 assert_eq!(result["pid"], 1234);
1462 }
1463
1464 #[test]
1465 fn test_metrics_output_to_response() {
1466 let output = MetricsOutput {
1467 requests_total: 100,
1468 errors_total: 5,
1469 lock_timeouts: 2,
1470 poison_recoveries: 0,
1471 uptime_ms: 10000,
1472 active_connections: 3,
1473 session_count: 2,
1474 };
1475 let response = metrics_output_to_response(1, output);
1476 let result = extract_result(response);
1477 assert_eq!(result["requests_total"], 100);
1478 assert_eq!(result["errors_total"], 5);
1479 }
1480
1481 #[test]
1482 fn test_console_output_to_response() {
1483 let output = ConsoleOutput {
1484 lines: vec!["line1".to_string(), "line2".to_string()],
1485 };
1486 let response = console_output_to_response(1, output);
1487 let result = extract_result(response);
1488 assert_eq!(result["line_count"], 2);
1489 }
1490
1491 #[test]
1492 fn test_pty_read_output_to_response() {
1493 let output = PtyReadOutput {
1494 session_id: SessionId::new("sess1"),
1495 data: "output".to_string(),
1496 bytes_read: 6,
1497 };
1498 let response = pty_read_output_to_response(1, output);
1499 let result = extract_result(response);
1500 assert_eq!(result["session_id"], "sess1");
1501 assert_eq!(result["bytes_read"], 6);
1502 }
1503
1504 #[test]
1505 fn test_pty_write_output_to_response() {
1506 let output = PtyWriteOutput {
1507 session_id: SessionId::new("sess1"),
1508 bytes_written: 5,
1509 success: true,
1510 };
1511 let response = pty_write_output_to_response(1, output);
1512 let result = extract_result(response);
1513 assert!(result["success"].as_bool().unwrap());
1514 assert_eq!(result["bytes_written"], 5);
1515 }
1516
1517 #[test]
1518 fn test_record_start_output_to_response() {
1519 let output = RecordStartOutput {
1520 session_id: SessionId::new("rec1"),
1521 success: true,
1522 };
1523 let response = record_start_output_to_response(1, output);
1524 let result = extract_result(response);
1525 assert!(result["recording"].as_bool().unwrap());
1526 }
1527
1528 #[test]
1529 fn test_get_text_output_to_response() {
1530 let output = GetTextOutput {
1531 found: true,
1532 text: "hello".to_string(),
1533 };
1534 let response = get_text_output_to_response(1, "@label", output);
1535 let result = extract_result(response);
1536 assert_eq!(result["ref"], "@label");
1537 assert_eq!(result["text"], "hello");
1538 assert!(result["found"].as_bool().unwrap());
1539 }
1540
1541 #[test]
1542 fn test_visibility_output_to_response() {
1543 let output = VisibilityOutput {
1544 found: true,
1545 visible: true,
1546 };
1547 let response = visibility_output_to_response(1, "@btn", output);
1548 let result = extract_result(response);
1549 assert_eq!(result["ref"], "@btn");
1550 assert!(result["visible"].as_bool().unwrap());
1551 }
1552
1553 #[test]
1554 fn test_toggle_output_to_response() {
1555 let output = ToggleOutput {
1556 success: true,
1557 checked: true,
1558 message: None,
1559 };
1560 let response = toggle_output_to_response(1, "@checkbox", output);
1561 let result = extract_result(response);
1562 assert!(result["checked"].as_bool().unwrap());
1563 }
1564
1565 #[test]
1566 fn test_scroll_into_view_output_to_response_success() {
1567 let output = ScrollIntoViewOutput {
1568 success: true,
1569 scrolls_needed: 3,
1570 message: None,
1571 };
1572 let response = scroll_into_view_output_to_response(1, "@item", output);
1573 let result = extract_result(response);
1574 assert!(result["success"].as_bool().unwrap());
1575 assert_eq!(result["scrolls_needed"], 3);
1576 }
1577
1578 #[test]
1579 fn test_scroll_into_view_output_to_response_failure() {
1580 let output = ScrollIntoViewOutput {
1581 success: false,
1582 scrolls_needed: 0,
1583 message: Some("Not found".to_string()),
1584 };
1585 let response = scroll_into_view_output_to_response(1, "@item", output);
1586 let result = extract_result(response);
1587 assert!(!result["success"].as_bool().unwrap());
1588 assert_eq!(result["message"], "Not found");
1589 }
1590
1591 #[test]
1592 fn test_multiselect_output_to_response() {
1593 let output = MultiselectOutput {
1594 success: true,
1595 selected_options: vec!["a".to_string(), "b".to_string()],
1596 message: None,
1597 };
1598 let response = multiselect_output_to_response(1, "@list", output);
1599 let result = extract_result(response);
1600 assert!(result["success"].as_bool().unwrap());
1601 assert_eq!(result["selected_options"].as_array().unwrap().len(), 2);
1602 }
1603
1604 #[test]
1605 fn test_get_title_output_to_response() {
1606 let output = GetTitleOutput {
1607 session_id: SessionId::new("sess1"),
1608 title: "My Terminal".to_string(),
1609 };
1610 let response = get_title_output_to_response(1, output);
1611 let result = extract_result(response);
1612 assert_eq!(result["session_id"], "sess1");
1613 assert_eq!(result["title"], "My Terminal");
1614 }
1615}