cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::*;
use crate::window::process::run_with_timeout;
use std::io::Write;
use std::process::Command;
use std::time::Duration;

#[test]
fn app_action_helper_typechecks_without_desktop_permissions() {
    let mut script = tempfile::NamedTempFile::with_suffix(".swift").unwrap();
    write!(script, "{}", source(include_str!("app_action.swift"))).unwrap();
    let output = Command::new("swiftc")
        .arg("-typecheck")
        .arg(script.path())
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn rejects_invalid_requests_before_starting_an_ax_helper() {
    for (pid, root, limit, depth) in [
        (0, None, 100, 3),
        (1, None, 0, 3),
        (1, None, 501, 3),
        (1, None, 100, 0),
        (1, None, 100, 13),
        (1, Some("window"), 100, 3),
    ] {
        assert!(inspect_app(pid, root, limit, depth).is_err());
    }
    assert!(press_app_element("invalid").is_err());
    let error = set_app_value("invalid", &"a".repeat(65_537)).unwrap_err();
    assert!(error.to_string().contains("65536"));
}

fn inspect_fixture(scenario: &str, root: Option<&str>) -> Result<serde_json::Value, String> {
    let mut script = tempfile::NamedTempFile::with_suffix(".swift").unwrap();
    write!(
        script,
        "{}\n{}",
        source(include_str!("root_fixture.swift")),
        include_str!("app_inspect.swift")
    )
    .unwrap();
    let input = serde_json::to_vec(&json!({"pid": std::process::id(), "root": root,
        "limit": 100, "depth": 3}))
    .unwrap();
    let output = run_with_timeout(
        Command::new("swift")
            .arg(script.path())
            .env("APP_AX_SCENARIO", scenario),
        &input,
        Duration::from_secs(40),
    )
    .unwrap();
    if output.status.success() {
        Ok(serde_json::from_slice(&output.stdout).unwrap())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).into_owned())
    }
}

#[test]
fn app_roots_support_windowless_menus_fallbacks_and_service_descendants() {
    let menu = inspect_fixture("windowless", Some("menu")).unwrap();
    assert_eq!(menu["nodes"][1]["role"], "AXMenuItem");
    for scenario in ["fallback", "deduplicate", "service"] {
        let snapshot = inspect_fixture(scenario, Some("w0")).unwrap();
        assert_eq!(snapshot["nodes"][0]["ref"], "w0");
        assert_eq!(snapshot["nodes"][1]["ref"], "w0.0");
        assert_eq!(snapshot["nodes"][1]["value"], "Fixture text");
        if scenario == "service" {
            assert_ne!(
                snapshot["nodes"][0]["receiver_pid"],
                snapshot["nodes"][1]["receiver_pid"]
            );
            continue;
        }
        let subtree = inspect_fixture(scenario, Some("w0.0")).unwrap();
        assert_eq!(
            subtree["nodes"][0]["fingerprint"],
            snapshot["nodes"][1]["fingerprint"]
        );
    }
}

#[test]
fn app_roots_reject_failed_reads_ambiguous_identity_and_locked_sessions() {
    for (scenario, message) in [
        ("ambiguous", "root identity is ambiguous"),
        ("failed-read", "app AX read failed"),
        ("failed-value", "app AX node read failed"),
        ("failed-role", "app AX node read failed for AXRole"),
        (
            "failed-identifier",
            "app AX node read failed for AXIdentifier",
        ),
        ("changed", "app context changed"),
        ("locked", "desktop is locked"),
    ] {
        let error = inspect_fixture(scenario, Some("w0")).unwrap_err();
        assert!(error.contains(message), "{scenario}: {error}");
    }
}

#[test]
fn partial_nodes_report_unavailable_attributes_and_cannot_issue_action_targets() {
    let raw = inspect_fixture("unavailable-value", Some("w0")).unwrap();
    let mut snapshot: AppAccessibilitySnapshot = serde_json::from_value(raw).unwrap();
    issue_targets(&mut snapshot, now().unwrap()).unwrap();
    let child = &snapshot.nodes[1];
    assert_eq!(child.unavailable_attributes, ["AXValue"]);
    assert!(child.node.value.is_none());
    assert!(!child.node.settable_value);
    assert!(child.node.actions.is_empty());
    assert!(child.node.target.is_none());
}

#[test]
fn optional_description_failure_preserves_verified_text_targets() {
    let raw = inspect_fixture("unavailable-description", Some("w0")).unwrap();
    let mut snapshot: AppAccessibilitySnapshot = serde_json::from_value(raw).unwrap();
    issue_targets(&mut snapshot, now().unwrap()).unwrap();
    let child = &snapshot.nodes[1];
    assert_eq!(child.unavailable_attributes, ["AXDescription"]);
    assert_eq!(child.node.value.as_deref(), Some("Fixture text"));
    assert!(child.node.settable_value);
    assert!(child.node.target.is_some());
}

#[test]
fn optional_identifier_failure_preserves_verified_text_targets() {
    for (scenario, missing) in [
        ("unavailable-identifier", vec!["AXIdentifier"]),
        (
            "unavailable-description-identifier",
            vec!["AXDescription", "AXIdentifier"],
        ),
    ] {
        let raw = inspect_fixture(scenario, Some("w0")).unwrap();
        let mut snapshot: AppAccessibilitySnapshot = serde_json::from_value(raw).unwrap();
        issue_targets(&mut snapshot, now().unwrap()).unwrap();
        let child = &snapshot.nodes[1];
        assert_eq!(child.unavailable_attributes, missing);
        assert!(child.node.identifier.is_none());
        assert_eq!(child.node.value.as_deref(), Some("Fixture text"));
        assert!(child.node.settable_value, "{scenario}");
        assert!(child.node.target.is_some(), "{scenario}");
    }
}

#[test]
fn app_nodes_reject_unknown_security_classification_before_reading_values() {
    for (scenario, message) in [
        ("missing-role", "AX node has no readable role"),
        ("malformed-role", "AX node has no readable role"),
        ("failed-subrole", "app AX node read failed for AXSubrole"),
        ("malformed-subrole", "AX node has no readable subrole"),
    ] {
        let error = inspect_fixture(scenario, Some("w0")).unwrap_err();
        assert!(error.contains(message), "{scenario}: {error}");
    }
}

#[test]
fn app_nodes_never_read_secure_values_or_issue_secure_targets() {
    for scenario in ["secure-subrole", "secure-subrole-unavailable-identifier"] {
        let raw = inspect_fixture(scenario, Some("w0")).unwrap();
        let mut snapshot: AppAccessibilitySnapshot = serde_json::from_value(raw).unwrap();
        issue_targets(&mut snapshot, now().unwrap()).unwrap();
        let child = &snapshot.nodes[1];
        assert!(child.node.value.is_none());
        assert!(!child.node.settable_value);
        assert!(child.node.actions.is_empty());
        assert!(child.node.target.is_none());
    }
}