cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::{ActionStatus, Target, now};
#[path = "action_test_support.rs"]
mod support;
use support::Fixture;

const FIELD: &str = "w0.0.0";

#[test]
fn app_dispatch_accepts_optional_identifier_failure_and_rechecks_availability() {
    let fixture = Fixture::build();
    for scenario in [
        "unavailable-identifier",
        "unavailable-description-identifier",
    ] {
        let target = fixture.observe(scenario, FIELD);
        assert_eq!(
            fixture.accept(scenario, &target, "press").status,
            ActionStatus::SentUnverified
        );
        assert_eq!(
            fixture.accept(scenario, &target, "set_value").status,
            ActionStatus::Confirmed
        );
        for action in ["press", "set_value"] {
            fixture.reject("valid", &target, action, "app element changed");
            let valid = fixture.observe("valid", FIELD);
            fixture.reject(scenario, &valid, action, "app element changed");
            fixture.reject(
                "post-lock-identifier",
                &valid,
                action,
                "app element changed before dispatch",
            );
        }
    }
}

#[test]
fn app_dispatch_accepts_optional_description_failure_and_rechecks_availability() {
    let fixture = Fixture::build();
    let target = fixture.observe("unavailable-description", FIELD);
    assert_eq!(
        fixture
            .accept("unavailable-description", &target, "press")
            .status,
        ActionStatus::SentUnverified
    );
    assert_eq!(
        fixture
            .accept("unavailable-description", &target, "set_value")
            .status,
        ActionStatus::Confirmed
    );
    for action in ["press", "set_value"] {
        fixture.reject("valid", &target, action, "app element changed");
        let valid = fixture.observe("valid", FIELD);
        fixture.reject(
            "unavailable-description",
            &valid,
            action,
            "app element changed",
        );
    }
}

#[test]
fn app_dispatch_rejects_stale_observations_without_sending_input() {
    let fixture = Fixture::build();
    let mutations: &[(&str, fn(&mut Target))] = &[
        ("application instance changed", |t| t.app.start_seconds += 1),
        ("app context changed", |t| t.context = "0".repeat(64)),
        ("app element changed", |t| {
            t.root_fingerprint = "0".repeat(64)
        }),
        ("app element changed", |t| t.fingerprint = "0".repeat(64)),
        ("target expired", |t| t.issued_at = now().unwrap() - 301),
        ("target expired", |t| t.issued_at = now().unwrap() + 60),
    ];
    for (message, mutate) in mutations {
        let mut target = fixture.observe("valid", FIELD);
        mutate(&mut target);
        for action in ["press", "set_value"] {
            fixture.reject("valid", &target, action, message);
        }
    }
    for action in ["press", "set_value"] {
        let target = fixture.observe("valid", FIELD);
        fixture.reject("receiver-instance", &target, action, "app element changed");
    }
}

#[test]
fn app_dispatch_rechecks_identity_context_and_nodes_after_locking() {
    let fixture = Fixture::build();
    let target = fixture.observe("valid", FIELD);
    for (scenario, message) in [
        ("post-lock-instance", "app context changed"),
        ("post-lock-context", "app context changed"),
        ("post-lock-receiver", "app element changed before dispatch"),
        ("post-lock-node", "app element changed before dispatch"),
        ("post-lock-ancestor", "app element changed before dispatch"),
        ("post-lock-element", "app element changed before dispatch"),
        ("post-lock-screen", "desktop is locked"),
    ] {
        for action in ["press", "set_value"] {
            fixture.reject(scenario, &target, action, message);
        }
    }
}

#[test]
fn app_dispatch_enforces_foreground_caller_and_control_guards() {
    let fixture = Fixture::build();
    let target = fixture.observe("valid", FIELD);
    for scenario in [
        "foreground-host",
        "foreground-receiver",
        "foreground-missing",
        "foreground-zero",
    ] {
        for action in ["press", "set_value"] {
            fixture.reject(scenario, &target, action, "background action stopped");
        }
    }
    for action in ["press", "set_value"] {
        fixture.reject("caller-exited", &target, action, "caller exited");
        let disabled = fixture.observe("disabled", FIELD);
        fixture.reject("disabled", &disabled, action, "element is disabled");
    }
    let unsupported = fixture.observe("unsupported-press", FIELD);
    fixture.reject(
        "unsupported-press",
        &unsupported,
        "press",
        "does not support AXPress",
    );
    let readonly = fixture.observe("readonly", FIELD);
    fixture.reject(
        "readonly",
        &readonly,
        "set_value",
        "does not support text value assignment",
    );
    let menu = fixture.observe("valid", "menu");
    fixture.reject("valid", &menu, "press", "only leaf menu items");
}

#[test]
fn app_dispatch_locks_both_processes_in_order_and_releases_after_failure() {
    let mut fixture = Fixture::build();
    for _ in 0..2 {
        let target = fixture.observe("valid", FIELD);
        let pressed = fixture.accept("valid", &target, "press");
        assert_eq!(pressed.status, ActionStatus::SentUnverified);
        let mut owners = vec![fixture.host, fixture.receiver];
        owners.sort();
        let expected: Vec<_> = owners
            .iter()
            .map(|pid| format!("lock:input-{pid}.lock"))
            .collect();
        assert_eq!(&fixture.events()[..2], expected);
        for owner in owners {
            let held = fixture.hold_lock(owner);
            for action in ["press", "set_value"] {
                fixture.reject("valid", &target, action, "another input action is running");
            }
            drop(held);
            fixture.accept("valid", &target, "press");
        }
        std::mem::swap(&mut fixture.host, &mut fixture.receiver);
    }
    fixture.receiver = fixture.host;
    let target = fixture.observe("valid", FIELD);
    fixture.accept("valid", &target, "press");
    assert_eq!(
        fixture
            .events()
            .iter()
            .filter(|e| e.starts_with("lock:"))
            .count(),
        1
    );
}

#[test]
fn app_dispatch_reports_readback_foreground_changes_and_uncertain_delivery() {
    let fixture = Fixture::build();
    let target = fixture.observe("valid", FIELD);
    let changed = fixture.accept("foreground-changed", &target, "press");
    assert!(changed.foreground_changed);
    assert_eq!(
        fixture.accept("valid", &target, "set_value").status,
        ActionStatus::Confirmed
    );
    assert_eq!(
        fixture.accept("unconfirmed", &target, "set_value").status,
        ActionStatus::SentUnverified
    );
    let menu = fixture.observe("valid", "menu.0");
    assert_eq!(
        fixture.accept("valid", &menu, "press").status,
        ActionStatus::SentUnverified
    );
    for action in ["press", "set_value"] {
        let output = fixture.act("dispatch-error", &target, action);
        assert!(!output.status.success());
        assert!(String::from_utf8_lossy(&output.stderr).contains("delivery is uncertain"));
        assert_eq!(
            fixture
                .events()
                .iter()
                .filter(|e| e.starts_with("dispatch:"))
                .count(),
            1
        );
    }
}