cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use crate::doctor::{DoctorCheck, DoctorCheckStatus};

pub(super) const AUTOMATION_FIX: &str =
    "Allow your terminal app in System Settings > Privacy & Security > Automation";
pub(super) const SAFARI_JS_FIX: &str =
    "Enable Safari Develop > Allow JavaScript from Apple Events";

pub(super) fn automation_check(
    id: &'static str,
    target: &'static str,
    result: Result<String, crate::MacosError>,
) -> DoctorCheck {
    match result {
        Ok(message) => DoctorCheck {
            id: id.to_string(),
            status: DoctorCheckStatus::Pass,
            target: target.to_string(),
            message,
            fix: None,
            required: false,
        },
        Err(error) => classify_automation_error(id, target, &error.to_string()),
    }
}

pub(super) fn live_safari_skip_check() -> DoctorCheck {
    DoctorCheck {
        id: "live.safari.js".to_string(),
        status: DoctorCheckStatus::Skip,
        target: "Safari JavaScript probe".to_string(),
        message: "live Safari probe skipped".to_string(),
        fix: None,
        required: false,
    }
}

pub(super) fn live_safari_check(result: Result<String, crate::MacosError>) -> DoctorCheck {
    match result {
        Ok(message) => DoctorCheck {
            id: "live.safari.js".to_string(),
            status: DoctorCheckStatus::Pass,
            target: "Safari JavaScript probe".to_string(),
            message,
            fix: None,
            required: false,
        },
        Err(error) => classify_live_safari_error(&error.to_string()),
    }
}

fn classify_automation_error(id: &'static str, target: &'static str, message: &str) -> DoctorCheck {
    if is_automation_denied(message) {
        DoctorCheck {
            id: id.to_string(),
            status: DoctorCheckStatus::Fail,
            target: target.to_string(),
            message: "automation access denied".to_string(),
            fix: Some(AUTOMATION_FIX.to_string()),
            required: false,
        }
    } else {
        DoctorCheck {
            id: id.to_string(),
            status: DoctorCheckStatus::Warn,
            target: target.to_string(),
            message: message.to_string(),
            fix: None,
            required: false,
        }
    }
}

fn classify_live_safari_error(message: &str) -> DoctorCheck {
    if is_javascript_automation_unavailable(message) {
        DoctorCheck {
            id: "live.safari.js".to_string(),
            status: DoctorCheckStatus::Warn,
            target: "Safari JavaScript probe".to_string(),
            message: "Safari JavaScript automation unavailable".to_string(),
            fix: Some(SAFARI_JS_FIX.to_string()),
            required: false,
        }
    } else if is_automation_denied(message) {
        DoctorCheck {
            id: "live.safari.js".to_string(),
            status: DoctorCheckStatus::Fail,
            target: "Safari JavaScript probe".to_string(),
            message: "automation access denied".to_string(),
            fix: Some(AUTOMATION_FIX.to_string()),
            required: false,
        }
    } else {
        DoctorCheck {
            id: "live.safari.js".to_string(),
            status: DoctorCheckStatus::Warn,
            target: "Safari JavaScript probe".to_string(),
            message: message.to_string(),
            fix: None,
            required: false,
        }
    }
}

fn is_automation_denied(message: &str) -> bool {
    let normalized = message.to_ascii_lowercase();
    normalized.contains("not allowed to send apple events")
        || normalized.contains("not authorized to send apple events")
        || normalized.contains("not permitted to send apple events")
        || normalized.contains("permission denied")
        || normalized.contains("1743")
}

fn is_javascript_automation_unavailable(message: &str) -> bool {
    let normalized = message.to_ascii_lowercase();
    normalized.contains("javascript from apple events")
        || normalized.contains("allow javascript from apple events")
        || normalized.contains("doesn't understand the do javascript message")
        || normalized.contains("does not understand the do javascript message")
        || normalized.contains("can't make current tab")
}

#[cfg(test)]
mod tests {
    use crate::doctor::DoctorCheckStatus;

    use super::{automation_check, live_safari_check, live_safari_skip_check};

    #[test]
    fn automation_denied_maps_to_fail_with_fix() {
        let check = automation_check(
            "automation.notes",
            "Notes",
            Err(crate::MacosError::Other(
                "osascript: Not authorized to send Apple events to Notes. (-1743)".to_string(),
            )),
        );

        assert_eq!(check.status, DoctorCheckStatus::Fail);
        assert!(check.fix.is_some());
    }

    #[test]
    fn live_safari_skip_check_is_skip() {
        let check = live_safari_skip_check();

        assert_eq!(check.status, DoctorCheckStatus::Skip);
    }

    #[test]
    fn javascript_automation_unavailable_maps_to_warn_with_fix() {
        let check = live_safari_check(Err(crate::MacosError::Other(
            "Safari JavaScript from Apple Events is disabled".to_string(),
        )));

        assert_eq!(check.status, DoctorCheckStatus::Warn);
        assert!(check.fix.is_some());
    }

    #[test]
    fn automation_classifier_does_not_flag_generic_authorization_word() {
        let check = automation_check(
            "automation.notes",
            "Notes",
            Err(crate::MacosError::Other(
                "OAuth authorization code expired".to_string(),
            )),
        );

        assert_eq!(check.status, DoctorCheckStatus::Warn);
        assert!(check.fix.is_none());
    }

    #[test]
    fn javascript_classifier_does_not_flag_generic_do_javascript_with_not_substring() {
        let check = live_safari_check(Err(crate::MacosError::Other(
            "do JavaScript returned notification marker".to_string(),
        )));

        assert_eq!(check.status, DoctorCheckStatus::Warn);
        assert!(check.fix.is_none());
    }
}