cueward-adapter-macos 0.5.0

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

use super::*;
use crate::window::AccessibilityNode;

#[test]
fn root_validation_rejects_ambiguous_and_unbounded_paths_before_inspection() {
    assert_eq!(root_depth("0").expect("root"), 0);
    assert_eq!(root_depth("0.2.17").expect("child"), 2);
    assert_eq!(
        root_depth(&format!("0{}", ".1".repeat(12))).expect("deepest"),
        12
    );
    for root in [
        "",
        "1",
        "0.",
        "0..1",
        "0.-1",
        "0.+1",
        "0.01",
        "0.text",
        "0.9223372036854775808",
    ] {
        assert!(
            inspect_window_subtree(0, root, 10, 2, false, false).is_err(),
            "{root}"
        );
        assert!(root_depth(root).is_err(), "{root}");
    }
    assert!(root_depth(&format!("0{}", ".1".repeat(13))).is_err());
    for (limit, depth) in [(0, 1), (501, 1), (1, 0), (1, 13)] {
        assert!(inspect_window_subtree(0, "0", limit, depth, false, false).is_err());
    }
}

#[test]
fn nodes_preserve_fractional_screen_bounds_and_optional_geometry() {
    let mut payload = serde_json::json!({
        "ref": "0.1", "role": "AXGroup", "name": "Editor", "actions": [],
        "settable_value": false, "fingerprint": "internal", "child_count": 2,
        "bounds": {"x": -800.25, "y": 40.5, "width": 300.75, "height": 120.0}
    });
    let node: AccessibilityNode = serde_json::from_value(payload.clone()).expect("node");
    assert_eq!(node.bounds.as_ref().expect("bounds").x, -800.25);
    assert_eq!(node.child_count, 2);
    let public = serde_json::to_value(node).expect("public node");
    assert!(public.get("fingerprint").is_none());
    assert_eq!(public["bounds"]["width"], 300.75);
    payload.as_object_mut().expect("object").remove("bounds");
    let node: AccessibilityNode = serde_json::from_value(payload).expect("no geometry");
    assert!(node.bounds.is_none());
    assert!(
        serde_json::to_value(node)
            .expect("node")
            .get("bounds")
            .is_none()
    );
}

fn inspect_fixture(root: &str, limit: usize, depth: usize) -> std::process::Output {
    let mut script = tempfile::NamedTempFile::with_suffix(".swift").expect("script");
    write!(
        script,
        "{}\n{}",
        include_str!("ax_inspection_fixture.swift"),
        include_str!("ax_inspect.swift")
    )
    .expect("write script");
    super::super::process::run_with_timeout(
        Command::new("swift")
            .arg(script.path())
            .args(["unused"; 7])
            .args([&limit.to_string(), &depth.to_string(), root]),
        b"",
        Duration::from_secs(40),
    )
    .expect("run traversal fixture")
}

fn inspect(root: &str, limit: usize, depth: usize) -> serde_json::Value {
    let output = inspect_fixture(root, limit, depth);
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).expect("inspection JSON")
}

#[test]
fn executable_traversal_explores_subtrees_with_absolute_refs_and_ancestor_identity() {
    let all = inspect("0", 20, 12);
    let subtree = inspect("0.0", 20, 12);
    let nodes = subtree["nodes"].as_array().expect("nodes");
    assert_eq!(subtree["root_ref"], "0.0");
    assert_eq!(subtree["truncated"], false);
    assert_eq!(
        nodes
            .iter()
            .map(|n| n["ref"].as_str().expect("ref"))
            .collect::<Vec<_>>(),
        ["0.0", "0.0.0", "0.0.1"]
    );
    assert_eq!(nodes[0]["parent_ref"], "0");
    assert_eq!(nodes[0]["child_count"], 2);
    assert_eq!(nodes[1]["child_count"], 0);
    for node in nodes {
        let original = all["nodes"]
            .as_array()
            .expect("nodes")
            .iter()
            .find(|n| n["ref"] == node["ref"])
            .expect("same ref");
        assert_eq!(node["fingerprint"], original["fingerprint"]);
    }
}

#[test]
fn executable_traversal_reports_limits_and_rejects_missing_roots() {
    let shallow = inspect("0", 20, 1);
    assert_eq!(shallow["truncated"], true);
    assert_eq!(shallow["nodes"].as_array().expect("nodes").len(), 3);
    let limited = inspect("0.0", 1, 12);
    assert_eq!(limited["truncated"], true);
    assert_eq!(limited["nodes"].as_array().expect("nodes").len(), 1);
    let leaf = inspect("0.0.0", 1, 0);
    assert_eq!(leaf["truncated"], false);
    let capped = inspect("0.0", 20, 0);
    assert_eq!(capped["truncated"], true);
    let missing = inspect_fixture("0.4", 20, 12);
    assert!(!missing.status.success());
    assert!(String::from_utf8_lossy(&missing.stderr).contains("root disappeared"));
}