1pub mod applescript;
2pub mod cliclick;
3pub mod hammerspoon;
4pub mod input_source;
5pub mod process;
6
7use crate::backend::hammerspoon::HammerspoonAxBackend;
8use crate::backend::process::ProcessRunner;
9use crate::error::CliError;
10use crate::model::{
11 AxActionPerformRequest, AxActionPerformResult, AxAttrGetRequest, AxAttrGetResult,
12 AxAttrSetRequest, AxAttrSetResult, AxClickRequest, AxClickResult, AxListRequest, AxListResult,
13 AxSessionListResult, AxSessionStartRequest, AxSessionStartResult, AxSessionStopRequest,
14 AxSessionStopResult, AxTypeRequest, AxTypeResult, AxWatchPollRequest, AxWatchPollResult,
15 AxWatchStartRequest, AxWatchStartResult, AxWatchStopRequest, AxWatchStopResult,
16};
17use crate::test_mode;
18
19const AX_EXTENDED_CAPABILITY_HINT: &str =
20 "AX attr/action/session/watch commands require Hammerspoon backend (`hs`).";
21const AX_EXTENDED_CAPABILITY_ACTION_HINT: &str = "Use `AGENTS_MACOS_AGENT_AX_BACKEND=hammerspoon|auto` and run `macos-agent preflight --include-probes` to verify readiness.";
22
23pub trait AxBackendAdapter {
24 fn list(
25 &self,
26 runner: &dyn ProcessRunner,
27 request: &AxListRequest,
28 timeout_ms: u64,
29 ) -> Result<AxListResult, CliError>;
30
31 fn click(
32 &self,
33 runner: &dyn ProcessRunner,
34 request: &AxClickRequest,
35 timeout_ms: u64,
36 ) -> Result<AxClickResult, CliError>;
37
38 fn type_text(
39 &self,
40 runner: &dyn ProcessRunner,
41 request: &AxTypeRequest,
42 timeout_ms: u64,
43 ) -> Result<AxTypeResult, CliError>;
44
45 fn attr_get(
46 &self,
47 _runner: &dyn ProcessRunner,
48 _request: &AxAttrGetRequest,
49 _timeout_ms: u64,
50 ) -> Result<AxAttrGetResult, CliError> {
51 Err(
52 CliError::runtime("AX attribute get is not supported by this backend")
53 .with_operation("ax.attr.get")
54 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
55 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
56 )
57 }
58
59 fn attr_set(
60 &self,
61 _runner: &dyn ProcessRunner,
62 _request: &AxAttrSetRequest,
63 _timeout_ms: u64,
64 ) -> Result<AxAttrSetResult, CliError> {
65 Err(
66 CliError::runtime("AX attribute set is not supported by this backend")
67 .with_operation("ax.attr.set")
68 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
69 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
70 )
71 }
72
73 fn action_perform(
74 &self,
75 _runner: &dyn ProcessRunner,
76 _request: &AxActionPerformRequest,
77 _timeout_ms: u64,
78 ) -> Result<AxActionPerformResult, CliError> {
79 Err(
80 CliError::runtime("AX action perform is not supported by this backend")
81 .with_operation("ax.action.perform")
82 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
83 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
84 )
85 }
86
87 fn session_start(
88 &self,
89 _runner: &dyn ProcessRunner,
90 _request: &AxSessionStartRequest,
91 _timeout_ms: u64,
92 ) -> Result<AxSessionStartResult, CliError> {
93 Err(
94 CliError::runtime("AX session start is not supported by this backend")
95 .with_operation("ax.session.start")
96 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
97 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
98 )
99 }
100
101 fn session_list(
102 &self,
103 _runner: &dyn ProcessRunner,
104 _timeout_ms: u64,
105 ) -> Result<AxSessionListResult, CliError> {
106 Err(
107 CliError::runtime("AX session list is not supported by this backend")
108 .with_operation("ax.session.list")
109 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
110 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
111 )
112 }
113
114 fn session_stop(
115 &self,
116 _runner: &dyn ProcessRunner,
117 _request: &AxSessionStopRequest,
118 _timeout_ms: u64,
119 ) -> Result<AxSessionStopResult, CliError> {
120 Err(
121 CliError::runtime("AX session stop is not supported by this backend")
122 .with_operation("ax.session.stop")
123 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
124 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
125 )
126 }
127
128 fn watch_start(
129 &self,
130 _runner: &dyn ProcessRunner,
131 _request: &AxWatchStartRequest,
132 _timeout_ms: u64,
133 ) -> Result<AxWatchStartResult, CliError> {
134 Err(
135 CliError::runtime("AX watch start is not supported by this backend")
136 .with_operation("ax.watch.start")
137 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
138 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
139 )
140 }
141
142 fn watch_poll(
143 &self,
144 _runner: &dyn ProcessRunner,
145 _request: &AxWatchPollRequest,
146 _timeout_ms: u64,
147 ) -> Result<AxWatchPollResult, CliError> {
148 Err(
149 CliError::runtime("AX watch poll is not supported by this backend")
150 .with_operation("ax.watch.poll")
151 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
152 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
153 )
154 }
155
156 fn watch_stop(
157 &self,
158 _runner: &dyn ProcessRunner,
159 _request: &AxWatchStopRequest,
160 _timeout_ms: u64,
161 ) -> Result<AxWatchStopResult, CliError> {
162 Err(
163 CliError::runtime("AX watch stop is not supported by this backend")
164 .with_operation("ax.watch.stop")
165 .with_hint(AX_EXTENDED_CAPABILITY_HINT)
166 .with_hint(AX_EXTENDED_CAPABILITY_ACTION_HINT),
167 )
168 }
169}
170
171#[derive(Debug, Default, Clone, Copy)]
172pub struct AppleScriptAxBackend;
173
174impl AxBackendAdapter for AppleScriptAxBackend {
175 fn list(
176 &self,
177 runner: &dyn ProcessRunner,
178 request: &AxListRequest,
179 timeout_ms: u64,
180 ) -> Result<AxListResult, CliError> {
181 applescript::ax_list(runner, request, timeout_ms)
182 }
183
184 fn click(
185 &self,
186 runner: &dyn ProcessRunner,
187 request: &AxClickRequest,
188 timeout_ms: u64,
189 ) -> Result<AxClickResult, CliError> {
190 applescript::ax_click(runner, request, timeout_ms)
191 }
192
193 fn type_text(
194 &self,
195 runner: &dyn ProcessRunner,
196 request: &AxTypeRequest,
197 timeout_ms: u64,
198 ) -> Result<AxTypeResult, CliError> {
199 applescript::ax_type(runner, request, timeout_ms)
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum AxBackendPreference {
205 Auto,
206 Hammerspoon,
207 AppleScript,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct AxBackendCapabilityCheck {
212 pub message: String,
213 pub hint: Option<String>,
214}
215
216impl AxBackendPreference {
217 pub fn as_str(self) -> &'static str {
218 match self {
219 Self::Auto => "auto",
220 Self::Hammerspoon => "hammerspoon",
221 Self::AppleScript => "applescript",
222 }
223 }
224
225 pub fn resolve() -> Self {
226 if let Ok(raw) = std::env::var("AGENTS_MACOS_AGENT_AX_BACKEND") {
227 match raw.trim().to_ascii_lowercase().as_str() {
228 "hammerspoon" | "hs" => return Self::Hammerspoon,
229 "applescript" | "jxa" => return Self::AppleScript,
230 "auto" => return Self::Auto,
231 _ => {}
232 }
233 }
234
235 if test_mode::enabled() {
236 Self::AppleScript
238 } else {
239 Self::Auto
240 }
241 }
242
243 fn capability_message(self) -> &'static str {
244 match self {
245 Self::Auto => {
246 "AX backend preference=auto; list/click/type use Hammerspoon first and may fallback to AppleScript (JXA). attr/action/session/watch remain Hammerspoon-only."
247 }
248 Self::Hammerspoon => {
249 "AX backend preference=hammerspoon; list/click/type and attr/action/session/watch all use Hammerspoon."
250 }
251 Self::AppleScript => {
252 "AX backend preference=applescript; list/click/type use AppleScript (JXA), while attr/action/session/watch still require Hammerspoon."
253 }
254 }
255 }
256}
257
258pub fn preflight_capability_check() -> AxBackendCapabilityCheck {
259 let preference = AxBackendPreference::resolve();
260 let hint = if preference == AxBackendPreference::Hammerspoon {
261 None
262 } else {
263 Some(AX_EXTENDED_CAPABILITY_ACTION_HINT.to_string())
264 };
265 AxBackendCapabilityCheck {
266 message: preference.capability_message().to_string(),
267 hint,
268 }
269}
270
271#[derive(Debug, Clone, Copy)]
272pub struct AutoAxBackend {
273 preference: AxBackendPreference,
274}
275
276impl Default for AutoAxBackend {
277 fn default() -> Self {
278 Self {
279 preference: AxBackendPreference::resolve(),
280 }
281 }
282}
283
284impl AutoAxBackend {
285 fn fallback_with_hint(primary_error: CliError, fallback_error: CliError) -> CliError {
286 fallback_error.with_hint(format!(
287 "Hammerspoon backend failed first: {}",
288 primary_error.message()
289 ))
290 }
291}
292
293impl AxBackendAdapter for AutoAxBackend {
294 fn list(
295 &self,
296 runner: &dyn ProcessRunner,
297 request: &AxListRequest,
298 timeout_ms: u64,
299 ) -> Result<AxListResult, CliError> {
300 match self.preference {
301 AxBackendPreference::Hammerspoon => {
302 HammerspoonAxBackend.list(runner, request, timeout_ms)
303 }
304 AxBackendPreference::AppleScript => {
305 AppleScriptAxBackend.list(runner, request, timeout_ms)
306 }
307 AxBackendPreference::Auto => match HammerspoonAxBackend
308 .list(runner, request, timeout_ms)
309 {
310 Ok(result) => Ok(result),
311 Err(primary_error) if hammerspoon::is_backend_unavailable_error(&primary_error) => {
312 AppleScriptAxBackend
313 .list(runner, request, timeout_ms)
314 .map_err(|fallback_error| {
315 Self::fallback_with_hint(primary_error, fallback_error)
316 })
317 }
318 Err(error) => Err(error),
319 },
320 }
321 }
322
323 fn click(
324 &self,
325 runner: &dyn ProcessRunner,
326 request: &AxClickRequest,
327 timeout_ms: u64,
328 ) -> Result<AxClickResult, CliError> {
329 match self.preference {
330 AxBackendPreference::Hammerspoon => {
331 HammerspoonAxBackend.click(runner, request, timeout_ms)
332 }
333 AxBackendPreference::AppleScript => {
334 AppleScriptAxBackend.click(runner, request, timeout_ms)
335 }
336 AxBackendPreference::Auto => match HammerspoonAxBackend
337 .click(runner, request, timeout_ms)
338 {
339 Ok(result) => Ok(result),
340 Err(primary_error) if hammerspoon::is_backend_unavailable_error(&primary_error) => {
341 AppleScriptAxBackend
342 .click(runner, request, timeout_ms)
343 .map_err(|fallback_error| {
344 Self::fallback_with_hint(primary_error, fallback_error)
345 })
346 }
347 Err(error) => Err(error),
348 },
349 }
350 }
351
352 fn type_text(
353 &self,
354 runner: &dyn ProcessRunner,
355 request: &AxTypeRequest,
356 timeout_ms: u64,
357 ) -> Result<AxTypeResult, CliError> {
358 match self.preference {
359 AxBackendPreference::Hammerspoon => {
360 HammerspoonAxBackend.type_text(runner, request, timeout_ms)
361 }
362 AxBackendPreference::AppleScript => {
363 AppleScriptAxBackend.type_text(runner, request, timeout_ms)
364 }
365 AxBackendPreference::Auto => {
366 match HammerspoonAxBackend.type_text(runner, request, timeout_ms) {
367 Ok(result) => Ok(result),
368 Err(primary_error)
369 if hammerspoon::is_backend_unavailable_error(&primary_error) =>
370 {
371 AppleScriptAxBackend
372 .type_text(runner, request, timeout_ms)
373 .map_err(|fallback_error| {
374 Self::fallback_with_hint(primary_error, fallback_error)
375 })
376 }
377 Err(error) => Err(error),
378 }
379 }
380 }
381 }
382
383 fn attr_get(
384 &self,
385 runner: &dyn ProcessRunner,
386 request: &AxAttrGetRequest,
387 timeout_ms: u64,
388 ) -> Result<AxAttrGetResult, CliError> {
389 HammerspoonAxBackend.attr_get(runner, request, timeout_ms)
390 }
391
392 fn attr_set(
393 &self,
394 runner: &dyn ProcessRunner,
395 request: &AxAttrSetRequest,
396 timeout_ms: u64,
397 ) -> Result<AxAttrSetResult, CliError> {
398 HammerspoonAxBackend.attr_set(runner, request, timeout_ms)
399 }
400
401 fn action_perform(
402 &self,
403 runner: &dyn ProcessRunner,
404 request: &AxActionPerformRequest,
405 timeout_ms: u64,
406 ) -> Result<AxActionPerformResult, CliError> {
407 HammerspoonAxBackend.action_perform(runner, request, timeout_ms)
408 }
409
410 fn session_start(
411 &self,
412 runner: &dyn ProcessRunner,
413 request: &AxSessionStartRequest,
414 timeout_ms: u64,
415 ) -> Result<AxSessionStartResult, CliError> {
416 HammerspoonAxBackend.session_start(runner, request, timeout_ms)
417 }
418
419 fn session_list(
420 &self,
421 runner: &dyn ProcessRunner,
422 timeout_ms: u64,
423 ) -> Result<AxSessionListResult, CliError> {
424 HammerspoonAxBackend.session_list(runner, timeout_ms)
425 }
426
427 fn session_stop(
428 &self,
429 runner: &dyn ProcessRunner,
430 request: &AxSessionStopRequest,
431 timeout_ms: u64,
432 ) -> Result<AxSessionStopResult, CliError> {
433 HammerspoonAxBackend.session_stop(runner, request, timeout_ms)
434 }
435
436 fn watch_start(
437 &self,
438 runner: &dyn ProcessRunner,
439 request: &AxWatchStartRequest,
440 timeout_ms: u64,
441 ) -> Result<AxWatchStartResult, CliError> {
442 HammerspoonAxBackend.watch_start(runner, request, timeout_ms)
443 }
444
445 fn watch_poll(
446 &self,
447 runner: &dyn ProcessRunner,
448 request: &AxWatchPollRequest,
449 timeout_ms: u64,
450 ) -> Result<AxWatchPollResult, CliError> {
451 HammerspoonAxBackend.watch_poll(runner, request, timeout_ms)
452 }
453
454 fn watch_stop(
455 &self,
456 runner: &dyn ProcessRunner,
457 request: &AxWatchStopRequest,
458 timeout_ms: u64,
459 ) -> Result<AxWatchStopResult, CliError> {
460 HammerspoonAxBackend.watch_stop(runner, request, timeout_ms)
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use nils_test_support::{EnvGuard, GlobalStateLock};
467 use pretty_assertions::assert_eq;
468 use serde_json::json;
469
470 use crate::backend::process::RealProcessRunner;
471 use crate::backend::{
472 AppleScriptAxBackend, AutoAxBackend, AxBackendAdapter, AxBackendPreference,
473 };
474 use crate::model::{
475 AxActionPerformRequest, AxAttrGetRequest, AxAttrSetRequest, AxClickRequest, AxListRequest,
476 AxSelector, AxSessionStartRequest, AxSessionStopRequest, AxTarget, AxTypeRequest,
477 AxWatchPollRequest, AxWatchStartRequest, AxWatchStopRequest,
478 };
479
480 fn node_selector() -> AxSelector {
481 AxSelector {
482 node_id: Some("1.1".to_string()),
483 ..AxSelector::default()
484 }
485 }
486
487 #[test]
488 fn backend_preference_defaults_to_applescript_in_test_mode() {
489 let lock = GlobalStateLock::new();
490 let _test_mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
491 let _backend = EnvGuard::remove(&lock, "AGENTS_MACOS_AGENT_AX_BACKEND");
492 assert_eq!(
493 AxBackendPreference::resolve(),
494 AxBackendPreference::AppleScript
495 );
496 }
497
498 #[test]
499 fn backend_preference_env_overrides_test_mode_default() {
500 let lock = GlobalStateLock::new();
501 let _test_mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
502 let _backend = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_AX_BACKEND", "hammerspoon");
503 assert_eq!(
504 AxBackendPreference::resolve(),
505 AxBackendPreference::Hammerspoon
506 );
507 }
508
509 #[test]
510 fn applescript_backend_reports_unsupported_for_ax_extension_methods() {
511 let runner = RealProcessRunner;
512 let request_target = AxTarget::default();
513 let selector = node_selector();
514
515 let attr_get = AppleScriptAxBackend.attr_get(
516 &runner,
517 &AxAttrGetRequest {
518 target: request_target.clone(),
519 selector: selector.clone(),
520 name: "AXRole".to_string(),
521 },
522 1000,
523 );
524 assert!(attr_get.is_err());
525
526 let attr_set = AppleScriptAxBackend.attr_set(
527 &runner,
528 &AxAttrSetRequest {
529 target: request_target.clone(),
530 selector: selector.clone(),
531 name: "AXValue".to_string(),
532 value: json!("hello"),
533 },
534 1000,
535 );
536 assert!(attr_set.is_err());
537
538 let action = AppleScriptAxBackend.action_perform(
539 &runner,
540 &AxActionPerformRequest {
541 target: request_target,
542 selector,
543 name: "AXPress".to_string(),
544 },
545 1000,
546 );
547 assert!(action.is_err());
548 }
549
550 #[test]
551 fn auto_backend_hammerspoon_preference_routes_list_click_type() {
552 let lock = GlobalStateLock::new();
553 let _test_mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
554 let _backend = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_AX_BACKEND", "hammerspoon");
555 let _list = EnvGuard::set(
556 &lock,
557 "AGENTS_MACOS_AGENT_AX_LIST_JSON",
558 r#"{"nodes":[{"node_id":"1.1","role":"AXButton","enabled":true,"focused":false,"actions":[],"path":["1","1"]}],"warnings":[]}"#,
559 );
560 let _click = EnvGuard::set(
561 &lock,
562 "AGENTS_MACOS_AGENT_AX_CLICK_JSON",
563 r#"{"node_id":"1.1","matched_count":1,"action":"ax-press","used_coordinate_fallback":false}"#,
564 );
565 let _typ = EnvGuard::set(
566 &lock,
567 "AGENTS_MACOS_AGENT_AX_TYPE_JSON",
568 r#"{"node_id":"1.1","matched_count":1,"applied_via":"ax-set-value","text_length":4,"submitted":false,"used_keyboard_fallback":false}"#,
569 );
570
571 let backend = AutoAxBackend::default();
572 let runner = RealProcessRunner;
573 let list = backend
574 .list(&runner, &AxListRequest::default(), 1000)
575 .expect("list should succeed");
576 assert_eq!(list.nodes.len(), 1);
577
578 let click = backend
579 .click(
580 &runner,
581 &AxClickRequest {
582 target: AxTarget::default(),
583 selector: node_selector(),
584 allow_coordinate_fallback: false,
585 reselect_before_click: false,
586 fallback_order: Vec::new(),
587 },
588 1000,
589 )
590 .expect("click should succeed");
591 assert_eq!(click.matched_count, 1);
592
593 let typ = backend
594 .type_text(
595 &runner,
596 &AxTypeRequest {
597 target: AxTarget::default(),
598 selector: node_selector(),
599 text: "test".to_string(),
600 clear_first: false,
601 submit: false,
602 paste: false,
603 allow_keyboard_fallback: false,
604 },
605 1000,
606 )
607 .expect("type should succeed");
608 assert_eq!(typ.text_length, 4);
609 }
610
611 #[test]
612 fn auto_backend_auto_preference_uses_hammerspoon_first_when_available() {
613 let lock = GlobalStateLock::new();
614 let _test_mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
615 let _backend = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_AX_BACKEND", "auto");
616 let _list = EnvGuard::set(
617 &lock,
618 "AGENTS_MACOS_AGENT_AX_LIST_JSON",
619 r#"{"nodes":[{"node_id":"9.9","role":"AXButton","enabled":true,"focused":false,"actions":[],"path":["9","9"]}],"warnings":[]}"#,
620 );
621
622 let backend = AutoAxBackend::default();
623 let runner = RealProcessRunner;
624 let list = backend
625 .list(&runner, &AxListRequest::default(), 1000)
626 .expect("list should succeed");
627 assert_eq!(list.nodes[0].node_id, "9.9");
628 }
629
630 #[test]
631 fn auto_backend_ax_extension_methods_route_through_hammerspoon() {
632 let lock = GlobalStateLock::new();
633 let _test_mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
634 let _backend = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_AX_BACKEND", "applescript");
635
636 let _attr_get = EnvGuard::set(
637 &lock,
638 "AGENTS_MACOS_AGENT_AX_ATTR_GET_JSON",
639 r#"{"node_id":"1.1","matched_count":1,"name":"AXRole","value":"AXButton"}"#,
640 );
641 let _attr_set = EnvGuard::set(
642 &lock,
643 "AGENTS_MACOS_AGENT_AX_ATTR_SET_JSON",
644 r#"{"node_id":"1.1","matched_count":1,"name":"AXValue","applied":true,"value_type":"string"}"#,
645 );
646 let _action = EnvGuard::set(
647 &lock,
648 "AGENTS_MACOS_AGENT_AX_ACTION_PERFORM_JSON",
649 r#"{"node_id":"1.1","matched_count":1,"name":"AXPress","performed":true}"#,
650 );
651 let _session_start = EnvGuard::set(
652 &lock,
653 "AGENTS_MACOS_AGENT_AX_SESSION_START_JSON",
654 r#"{"session_id":"axs-1","app":"Arc","bundle_id":"company.thebrowser.Browser","pid":1001,"created_at_ms":1700000000000,"created":true}"#,
655 );
656 let _session_list = EnvGuard::set(
657 &lock,
658 "AGENTS_MACOS_AGENT_AX_SESSION_LIST_JSON",
659 r#"{"sessions":[{"session_id":"axs-1","app":"Arc","bundle_id":"company.thebrowser.Browser","pid":1001,"created_at_ms":1700000000000}]}"#,
660 );
661 let _session_stop = EnvGuard::set(
662 &lock,
663 "AGENTS_MACOS_AGENT_AX_SESSION_STOP_JSON",
664 r#"{"session_id":"axs-1","removed":true}"#,
665 );
666 let _watch_start = EnvGuard::set(
667 &lock,
668 "AGENTS_MACOS_AGENT_AX_WATCH_START_JSON",
669 r#"{"watch_id":"axw-1","session_id":"axs-1","events":["AXTitleChanged"],"max_buffer":64,"started":true}"#,
670 );
671 let _watch_poll = EnvGuard::set(
672 &lock,
673 "AGENTS_MACOS_AGENT_AX_WATCH_POLL_JSON",
674 r#"{"watch_id":"axw-1","events":[],"dropped":0,"running":true}"#,
675 );
676 let _watch_stop = EnvGuard::set(
677 &lock,
678 "AGENTS_MACOS_AGENT_AX_WATCH_STOP_JSON",
679 r#"{"watch_id":"axw-1","stopped":true,"drained":0}"#,
680 );
681
682 let backend = AutoAxBackend::default();
683 let runner = RealProcessRunner;
684
685 let attr_get = backend
686 .attr_get(
687 &runner,
688 &AxAttrGetRequest {
689 target: AxTarget::default(),
690 selector: node_selector(),
691 name: "AXRole".to_string(),
692 },
693 1000,
694 )
695 .expect("attr get should succeed");
696 assert_eq!(attr_get.name, "AXRole");
697
698 let attr_set = backend
699 .attr_set(
700 &runner,
701 &AxAttrSetRequest {
702 target: AxTarget::default(),
703 selector: node_selector(),
704 name: "AXValue".to_string(),
705 value: json!("hello"),
706 },
707 1000,
708 )
709 .expect("attr set should succeed");
710 assert!(attr_set.applied);
711
712 let action = backend
713 .action_perform(
714 &runner,
715 &AxActionPerformRequest {
716 target: AxTarget::default(),
717 selector: node_selector(),
718 name: "AXPress".to_string(),
719 },
720 1000,
721 )
722 .expect("action should succeed");
723 assert!(action.performed);
724
725 let start = backend
726 .session_start(
727 &runner,
728 &AxSessionStartRequest {
729 target: AxTarget::default(),
730 session_id: Some("axs-1".to_string()),
731 },
732 1000,
733 )
734 .expect("session start should succeed");
735 assert_eq!(start.session.session_id, "axs-1");
736
737 let listed = backend
738 .session_list(&runner, 1000)
739 .expect("session list should succeed");
740 assert_eq!(listed.sessions.len(), 1);
741
742 let stop = backend
743 .session_stop(
744 &runner,
745 &AxSessionStopRequest {
746 session_id: "axs-1".to_string(),
747 },
748 1000,
749 )
750 .expect("session stop should succeed");
751 assert!(stop.removed);
752
753 let watch_start = backend
754 .watch_start(
755 &runner,
756 &AxWatchStartRequest {
757 session_id: "axs-1".to_string(),
758 events: vec!["AXTitleChanged".to_string()],
759 max_buffer: 64,
760 watch_id: Some("axw-1".to_string()),
761 },
762 1000,
763 )
764 .expect("watch start should succeed");
765 assert_eq!(watch_start.watch_id, "axw-1");
766
767 let watch_poll = backend
768 .watch_poll(
769 &runner,
770 &AxWatchPollRequest {
771 watch_id: "axw-1".to_string(),
772 limit: 10,
773 drain: true,
774 },
775 1000,
776 )
777 .expect("watch poll should succeed");
778 assert!(watch_poll.running);
779
780 let watch_stop = backend
781 .watch_stop(
782 &runner,
783 &AxWatchStopRequest {
784 watch_id: "axw-1".to_string(),
785 },
786 1000,
787 )
788 .expect("watch stop should succeed");
789 assert!(watch_stop.stopped);
790 }
791}