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::screenshot::{CapturableWindow, WindowBounds};
use crate::window::{snapshot::SnapshotImage, target::now_seconds};
use serde_json::json;

fn window() -> CapturableWindow {
    CapturableWindow {
        window_id: 42,
        app: "Fixture".into(),
        title: "Fixture window".into(),
        owner_pid: 123,
        is_frontmost: false,
        is_onscreen: false,
        bounds: WindowBounds {
            x: -300,
            y: 20,
            width: 640,
            height: 480,
        },
    }
}

#[test]
fn space_target_binds_identity_membership_and_expiry_without_image_coordinates() {
    let window = WindowIdentity::from(&window());
    let token = MoveTarget::issue(window.clone(), vec![9, 3], 1000).unwrap();
    for now in [1000, 1300] {
        let decoded = MoveTarget::decode(&token, now).unwrap();
        assert_eq!(decoded.window, window);
        assert_eq!(decoded.expected_spaces, Some(vec![3, 9]));
    }
    for now in [999, 1301] {
        assert!(MoveTarget::decode(&token, now).is_err());
    }
    assert!(InputTarget::decode(&token, 1000).is_err());
}

#[test]
fn space_target_accepts_legacy_snapshot_observations_with_their_original_guards() {
    let image = SnapshotImage {
        width: 1280,
        height: 960,
        scale_x: 2.0,
        scale_y: 2.0,
        origin: "window_frame_top_left",
    };
    let token = InputTarget::issue(&window(), &image).unwrap();
    let now = now_seconds().unwrap();
    let decoded = MoveTarget::decode(&token, now).unwrap();
    assert_eq!(decoded.window, WindowIdentity::from(&window()));
    assert!(decoded.expected_spaces.is_none());
    assert!(MoveTarget::decode(&token, now + 301).is_err());
    let mut value: serde_json::Value =
        serde_json::from_slice(&URL_SAFE_NO_PAD.decode(token).unwrap()).unwrap();
    value["image_width"] = json!(0);
    let invalid = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&value).unwrap());
    assert!(MoveTarget::decode(&invalid, now).is_err());
}

#[test]
fn space_target_rejects_malformed_identity_membership_and_other_namespaces() {
    let token = MoveTarget::issue(WindowIdentity::from(&window()), vec![3], 1000).unwrap();
    let value: serde_json::Value =
        serde_json::from_slice(&URL_SAFE_NO_PAD.decode(token).unwrap()).unwrap();
    for (path, bad) in [
        ("/kind", json!("app_ax")),
        ("/kind", json!("window_input")),
        ("/version", json!(2)),
        ("/issued_at", json!(null)),
        ("/window/window_id", json!(0)),
        ("/window/owner_pid", json!(-1)),
        ("/window/bounds/width", json!(0)),
        ("/window/bounds/height", json!(-1)),
        ("/space_ids", json!([])),
        ("/space_ids", json!([0])),
        ("/space_ids", json!([3, 3])),
        ("/space_ids", json!([9, 3])),
        ("/space_ids", json!([true])),
        ("/space_ids", json!("3")),
        ("/space_ids", json!((1..=65).collect::<Vec<u64>>())),
    ] {
        let mut changed = value.clone();
        *changed.pointer_mut(path).unwrap() = bad;
        let token = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&changed).unwrap());
        assert!(
            MoveTarget::decode(&token, 1000).is_err(),
            "accepted {path}: {changed}"
        );
    }
    for token in ["invalid".to_owned(), "a".repeat(16_385)] {
        assert!(MoveTarget::decode(&token, 1000).is_err());
    }
    for ids in [vec![], vec![0], vec![3, 3]] {
        assert!(MoveTarget::issue(WindowIdentity::from(&window()), ids, 1000).is_err());
    }
}