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::path::Path;

fn compile_fixture(directory: &Path) -> std::path::PathBuf {
    let source = directory.join("creation.m");
    fs::write(
        &source,
        format!(
            "{}\n{}\n{}\n{}",
            include_str!("space_guards.m"),
            include_str!("space_create_fixture.m"),
            include_str!("space_create.m"),
            r#"int main(void) { @autoreleasepool {
                request = [NSJSONSerialization JSONObjectWithData:NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile options:0 error:nil];
                scenario = request[@"scenario"]; createSpace(request); return 0;
            } }"#
        ),
    )
    .unwrap();
    let binary = directory.join("creation");
    let output = Command::new("clang")
        .args(["-fobjc-arc", "-framework", "Cocoa"])
        .arg(source)
        .arg("-o")
        .arg(&binary)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    binary
}

fn observe(binary: &Path, directory: &Path, scenario: &str) -> (std::process::Output, Value) {
    let trace = directory.join("trace.json");
    let payload = json!({"scenario":scenario,"trace":trace,
        "lock_path":directory.join("creation.lock"),"caller_pid":std::process::id()});
    let output = run_with_timeout(
        &mut Command::new(binary),
        &serde_json::to_vec(&payload).unwrap(),
        Duration::from_secs(8),
    )
    .unwrap();
    let trace = serde_json::from_slice(&fs::read(trace).unwrap()).unwrap();
    (output, trace)
}

#[test]
fn creation_dispatches_once_and_confirms_only_its_new_inactive_desktop() {
    let directory = tempfile::tempdir().unwrap();
    let binary = compile_fixture(directory.path());
    for scenario in ["normal", "delayed", "foreground-changed"] {
        let (output, trace) = observe(&binary, directory.path(), scenario);
        assert!(
            output.status.success(),
            "{}",
            String::from_utf8_lossy(&output.stderr)
        );
        let result: SpaceCreateResult = serde_json::from_slice(&output.stdout).unwrap();
        assert_eq!(result.status, super::super::super::ActionStatus::Confirmed);
        assert_eq!(result.space_id, Some(99));
        assert_eq!(result.display_id.as_deref(), Some("fixture-display"));
        assert!(result.reason.is_none());
        assert_eq!(result.visible_spaces_before, vec![1]);
        assert_eq!(result.visible_spaces_after, Some(vec![1]));
        assert_eq!(result.visible_spaces_changed, Some(false));
        assert_eq!(result.foreground_changed, scenario == "foreground-changed");
        assert_eq!(trace["calls"], 1);
        assert_eq!(trace["options"], 0);
        assert_eq!(trace["values"]["type"], 0);
        assert_eq!(trace["values"]["uuid"].as_str().unwrap().len(), 36);
    }
    assert_rejections(&binary, directory.path());
    assert_uncertain(&binary, directory.path());
}

fn assert_rejections(binary: &Path, directory: &Path) {
    for (scenario, message) in [
        ("unavailable", "unavailable"),
        ("missing-before", "catalog"),
        ("no-foreground", "foreground"),
        ("dead-caller", "caller exited"),
        ("caller-exited", "before submission"),
        ("prepare-failed", "cannot prepare"),
    ] {
        let (output, trace) = observe(binary, directory, scenario);
        assert!(!output.status.success(), "{scenario}");
        assert!(
            String::from_utf8_lossy(&output.stderr).contains(message),
            "{scenario}: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        assert_eq!(trace["calls"], 0, "{scenario}");
    }
    let _lock = crate::window::input_lock::lock_path(&directory.join("creation.lock")).unwrap();
    let (output, trace) = observe(binary, directory, "normal");
    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("another Space creation"));
    assert_eq!(trace["calls"], 0);
}

fn assert_uncertain(binary: &Path, directory: &Path) {
    for scenario in [
        "zero",
        "reused",
        "missing-result",
        "absent",
        "wrong-uuid",
        "wrong-type",
        "visible",
        "duplicate",
        "missing-after",
    ] {
        let (output, trace) = observe(binary, directory, scenario);
        assert!(
            output.status.success(),
            "{scenario}: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        let result: SpaceCreateResult = serde_json::from_slice(&output.stdout).unwrap();
        assert_eq!(
            result.status,
            super::super::super::ActionStatus::SentUnverified,
            "{scenario}"
        );
        assert!(result.display_id.is_none() && result.reason.is_some());
        assert_eq!(
            result.space_id,
            if ["zero", "reused", "missing-result"].contains(&scenario) {
                None
            } else {
                Some(99)
            }
        );
        assert_eq!(trace["calls"], 1, "must not retry {scenario}");
        if scenario == "missing-after" {
            assert!(
                result.visible_spaces_after.is_none() && result.visible_spaces_changed.is_none()
            );
        } else {
            assert_eq!(result.visible_spaces_changed, Some(scenario == "visible"));
        }
    }
}