cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::*;
use std::fs;
use std::process::Command;

fn options(condition: WaitCondition) -> WaitOptions {
    WaitOptions {
        condition,
        selector: Some(WaitSelector {
            role: "AXTextField".into(),
            name: Some("Status".into()),
            identifier: None,
        }),
        value: None,
        timeout_ms: 4000,
        interval_ms: 100,
    }
}

#[test]
fn wait_options_reject_incomplete_and_unbounded_queries() {
    let mut request = options(WaitCondition::Enabled);
    assert!(validate(&request).is_ok());
    request.timeout_ms = 20_001;
    assert!(validate(&request).is_err());
    request.timeout_ms = 100;
    request.interval_ms = 0;
    assert!(validate(&request).is_err());
    request.interval_ms = 100;
    request.condition = WaitCondition::ValueEquals;
    assert!(validate(&request).is_err());
    request.value = Some(String::new());
    assert!(validate(&request).is_ok());
    request.condition = WaitCondition::WindowGone;
    assert!(validate(&request).is_err());
    request.selector = None;
    request.value = None;
    assert!(validate(&request).is_ok());
    request.condition = WaitCondition::ElementAbsent;
    assert!(validate(&request).is_err());
}

#[test]
fn wait_reports_missing_role_for_named_or_unqualified_element_queries() {
    let mut request = options(WaitCondition::ElementExists);
    for selector in [
        None,
        Some(WaitSelector {
            role: String::new(),
            name: Some("Continue".into()),
            identifier: None,
        }),
        Some(WaitSelector {
            role: String::new(),
            name: None,
            identifier: Some("status".into()),
        }),
    ] {
        request.selector = selector;
        assert_eq!(
            validate(&request).expect_err("role required").to_string(),
            "element conditions require --role"
        );
    }
}

#[test]
fn wait_reads_only_selector_attributes_and_preserves_required_read_failures() {
    let source = include_str!("ax_wait.swift");
    let scan = source
        .split_once("struct WaitScan {")
        .expect("scan entry")
        .1
        .split_once("\nfunc windowState()")
        .expect("scan end")
        .0;
    let directory = tempfile::tempdir().expect("fixture");
    let script = directory.path().join("wait.swift");
    fs::write(&script, format!("{}\n{}\nstruct WaitScan {{{scan}\n{}",
        include_str!("wait_selector_fixture.swift"),
        include_str!("wait_logic.swift"),
        r#"
var selector: [String: Any] = ["role": "AXTextArea"]
if scenario.hasPrefix("name") { selector["name"] = "Editor" }
if scenario == "identifier-failure" { selector["identifier"] = "editor" }
var scan = WaitScan(deadline: ProcessInfo.processInfo.systemUptime + 1, selector: selector, condition: "element-exists")
scan.walk(AXUIElement("AXTextArea"), ref: "0", depth: 0)
guard scan.matches.count == 1 else { fail("expected one matching node") }
print("matched")
"#)).expect("write fixture");
    for (scenario, expected) in [
        ("role-only", "matched"),
        ("name-title", "matched"),
        ("name-description-failure", "description failed"),
        ("identifier-failure", "identifier failed"),
        ("role-failure", "required role failed"),
    ] {
        let output = Command::new("swift")
            .arg(&script)
            .arg(scenario)
            .output()
            .expect("run selector fixture");
        if expected == "matched" {
            assert!(
                output.status.success(),
                "{scenario}: {}",
                String::from_utf8_lossy(&output.stderr)
            );
            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "matched");
        } else {
            assert!(
                !output.status.success(),
                "required attribute failure must not match"
            );
            assert!(String::from_utf8_lossy(&output.stderr).contains(expected));
        }
    }
}

#[test]
fn wait_classifies_window_lifecycle_from_consistent_catalog_observations() {
    let directory = tempfile::tempdir().expect("wait catalog fixture");
    let path = directory.path().join("wait.swift");
    fs::write(
        &path,
        [
            include_str!("ax_elements.swift"),
            include_str!("ax_shared.swift"),
            include_str!("ax_common.swift"),
            include_str!("wait_catalog_fixture.swift"),
            include_str!("wait_logic.swift"),
            include_str!("ax_wait.swift"),
        ]
        .join("\n"),
    )
    .expect("write catalog fixture");
    let request = serde_json::to_vec(&json!({
        "caller_pid": std::process::id(),
        "options": { "condition": "window-gone", "timeout_ms": 200, "interval_ms": 50 }
    }))
    .expect("request");
    for (scenario, expected) in [
        ("close", "matched"),
        ("absent", "matched"),
        ("present", "timed_out"),
        ("rename", "window_changed"),
        ("owner", "window_changed"),
        ("bounds", "window_changed"),
        ("fractional", "timed_out"),
        ("unavailable", "error"),
    ] {
        let output = crate::window::process::run_with_timeout(
            Command::new("swift")
                .arg(&path)
                .args(["123", "7", "Fixture", "-100", "200", "640", "480", scenario]),
            &request,
            std::time::Duration::from_secs(40),
        )
        .expect("run production wait loop");
        let stderr = String::from_utf8_lossy(&output.stderr);
        if expected == "error" {
            assert!(!output.status.success());
            assert!(stderr.contains("absence is unproven"), "{stderr}");
        } else {
            assert!(output.status.success(), "{scenario}: {stderr}");
            let result: serde_json::Value =
                serde_json::from_slice(&output.stdout).expect("wait result");
            assert_eq!(result["status"], expected, "{scenario}");
        }
    }
}

#[test]
fn wait_matching_requires_exact_values_unique_nodes_and_complete_absence() {
    let directory = tempfile::tempdir().expect("wait fixture");
    let path = directory.path().join("wait.swift");
    fs::write(
        &path,
        format!(
            "{}\n{}",
            include_str!("wait_logic.swift"),
            include_str!("wait_logic_tests.swift")
        ),
    )
    .expect("write wait fixture");
    let output = Command::new("swift")
        .arg(path)
        .output()
        .expect("run wait matching");
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "passed");
}

fn missing_window_target() -> String {
    use crate::screenshot::{CapturableWindow, WindowBounds};
    use crate::window::snapshot::SnapshotImage;
    let window = CapturableWindow {
        window_id: 999_999_999,
        owner_pid: std::process::id() as i32,
        app: "Wait Validation Fixture".into(),
        title: "Missing fixture window".into(),
        is_frontmost: false,
        is_onscreen: false,
        bounds: WindowBounds {
            x: 0,
            y: 0,
            width: 100,
            height: 100,
        },
    };
    let image = SnapshotImage {
        width: 100,
        height: 100,
        scale_x: 1.0,
        scale_y: 1.0,
        origin: "top-left",
    };
    InputTarget::issue(&window, &image).expect("fixture token")
}

#[test]
#[ignore = "requires Accessibility and Screen Recording permissions; sends no input"]
fn wait_rejects_unmatchable_names_before_observing_window() {
    let target = missing_window_target();
    for grapheme in ["a", "測", "e\u{301}"] {
        let mut request = options(WaitCondition::ElementAbsent);
        request.selector.as_mut().expect("selector").name = Some(grapheme.repeat(513));
        let error = wait_for_window(&target, &request).expect_err("unmatchable name rejected");
        assert!(error.to_string().contains("512"), "{error}");
    }
}

#[test]
#[ignore = "requires Accessibility and Screen Recording permissions; sends no input"]
fn wait_name_limit_counts_graphemes_and_preserves_full_identifiers() {
    let target = missing_window_target();
    let mut request = options(WaitCondition::ElementAbsent);
    let selector = request.selector.as_mut().expect("selector");
    selector.name = Some("e\u{301}".repeat(512));
    selector.identifier = Some("i".repeat(600));
    assert_eq!(
        wait_for_window(&target, &request)
            .expect("valid selector reaches catalog")
            .status,
        WaitStatus::WindowGone
    );
}