cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use super::*;
use cueward_core::files::relocation::{
    RelocationPlatform, RelocationReceipt, RelocationRequest, RenameOutcome,
};
use cueward_core::files::{FilePlatform, FileStamp, ResourceMetadata};
use std::cell::Cell;
use std::fs::{File, Metadata};
use std::io;

enum Change {
    Source,
    Target,
    Parent,
    Alias(std::path::PathBuf, std::path::PathBuf),
    Final,
    PendingLink,
    FinalLink,
    LinkTarget(std::path::PathBuf),
    Rejected,
    Unknown,
    Pause(std::path::PathBuf),
}
struct Racing {
    root: std::path::PathBuf,
    calls: Cell<usize>,
    change: Change,
}
impl FilePlatform for Racing {
    fn stamp(&self, m: &Metadata) -> FileStamp {
        MacFiles.stamp(m)
    }
    fn open_regular(&self, p: &Path) -> io::Result<File> {
        MacFiles.open_regular(p)
    }
    fn resource_metadata(&self, p: &Path, m: &Metadata) -> Result<ResourceMetadata, FileError> {
        MacFiles.resource_metadata(p, m)
    }
}
impl BatchRenamePlatform for Racing {
    fn names_may_collide(&self, a: &str, b: &str) -> bool {
        MacFiles.names_may_collide(a, b)
    }
}
impl RelocationPlatform for Racing {
    fn open_symlink(&self, p: &Path) -> Result<File, FileError> {
        RelocationPlatform::open_symlink(&MacFiles, p)
    }
    fn open_directory(&self, p: &Path) -> Result<File, FileError> {
        RelocationPlatform::open_directory(&MacFiles, p)
    }
    fn same_filesystem(&self, a: &Metadata, b: &Metadata) -> bool {
        RelocationPlatform::same_filesystem(&MacFiles, a, b)
    }
    fn prepare_rename(&self, p: &Path, r: &RelocationReceipt) -> Result<(), FileError> {
        RelocationPlatform::prepare_rename(&MacFiles, p, r)
    }
    fn rename(&self, root: &File, r: &RelocationRequest) -> RenameOutcome {
        let count = self.calls.get() + 1;
        self.calls.set(count);
        if matches!(self.change, Change::Rejected | Change::Unknown) {
            return RenameOutcome {
                result: Err(FileError::new(
                    FileErrorCode::Unavailable,
                    "injected native outcome",
                )),
                renamed: if matches!(self.change, Change::Unknown) {
                    None
                } else {
                    Some(false)
                },
            };
        }
        let outcome = RelocationPlatform::rename(&MacFiles, root, r);
        assert_eq!(outcome.renamed, Some(true));
        if count == 1 {
            match &self.change {
                Change::PendingLink => {
                    fs::rename(self.root.join("b"), self.root.join("saved-pending")).unwrap();
                    symlink("replacement", self.root.join("b")).unwrap();
                }
                Change::LinkTarget(target) => fs::write(target, b"OWNED changed target").unwrap(),
                Change::Source => fs::write(self.root.join("b"), b"OWNED changed").unwrap(),
                Change::Target => {
                    fs::write(self.root.join("other"), b"OWNED external target").unwrap()
                }
                Change::Parent => {
                    fs::write(self.root.join("nested/external"), b"OWNED external").unwrap()
                }
                Change::Alias(alias, foreign) => {
                    fs::remove_file(alias).unwrap();
                    symlink(foreign, alias).unwrap();
                }
                Change::Pause(directory) => {
                    fs::write(directory.join("ready"), b"first native rename submitted").unwrap();
                    lifetime::wait_for(|| directory.join("release").exists());
                }
                _ => (),
            }
        }
        if count == 2 && matches!(self.change, Change::Final) {
            fs::write(self.root.join("new"), b"OWNED externally changed result").unwrap();
        }
        if count == 2 && matches!(self.change, Change::FinalLink) {
            fs::rename(self.root.join("new"), self.root.join("saved-completed")).unwrap();
            symlink("replacement", self.root.join("new")).unwrap();
        }
        outcome
    }
}
#[test]
fn batch_execute_stops_pending_source_target_or_parent_changes_after_one_verified_child() {
    for change in [Change::Source, Change::Target, Change::Parent] {
        let root = fixture();
        fs::create_dir(root.path().join("nested")).unwrap();
        fs::write(root.path().join("nested/c"), b"OWNED c").unwrap();
        let second = if matches!(change, Change::Parent) {
            "nested/c"
        } else {
            "b"
        };
        let platform = Racing {
            root: root.path().into(),
            calls: Cell::new(0),
            change,
        };
        let r = perform(
            &platform,
            &request(root.path(), &[("a", "new"), (second, "other")]),
        );
        assert_eq!(r.status, RelocationStatus::Incomplete, "{:?}", r.error);
        assert!(r.items[0].completion_verified && r.items[1].operation_id.is_none());
        assert_eq!(platform.calls.get(), 1);
        assert_eq!(fs::read(root.path().join("new")).unwrap(), b"OWNED a\0");
        assert!(root.path().join(second).exists());
        if matches!(platform.change, Change::Target) {
            assert_eq!(
                fs::read(root.path().join("other")).unwrap(),
                b"OWNED external target"
            );
        }
        cleanup(&r);
    }
}
#[test]
fn batch_execute_root_alias_retarget_stops_without_touching_foreign_root() {
    let root = fixture();
    let foreign = fixture();
    let aliases = tempfile::tempdir().unwrap();
    let alias = aliases.path().join("root");
    symlink(root.path(), &alias).unwrap();
    let platform = Racing {
        root: root.path().into(),
        calls: Cell::new(0),
        change: Change::Alias(alias.clone(), foreign.path().into()),
    };
    let r = perform(&platform, &request(&alias, &[("a", "new"), ("b", "other")]));
    assert_eq!(r.status, RelocationStatus::Incomplete);
    assert_eq!(platform.calls.get(), 1);
    assert!(r.items[1].operation_id.is_none());
    assert_eq!(fs::read(foreign.path().join("a")).unwrap(), b"OWNED a\0");
    assert_eq!(fs::read(foreign.path().join("b")).unwrap(), b"OWNED b\0");
    assert!(!foreign.path().join("new").exists() && !foreign.path().join("other").exists());
    cleanup(&r);
}
#[test]
fn batch_execute_final_recheck_rejects_changed_completed_destination() {
    let root = fixture();
    let platform = Racing {
        root: root.path().into(),
        calls: Cell::new(0),
        change: Change::Final,
    };
    let r = perform(
        &platform,
        &request(root.path(), &[("a", "new"), ("b", "other")]),
    );
    assert_eq!(r.status, RelocationStatus::Incomplete);
    assert!(!r.completion_verified && r.items.iter().all(|i| i.completion_verified));
    assert_eq!(platform.calls.get(), 2);
    assert_eq!(
        fs::read(root.path().join("new")).unwrap(),
        b"OWNED externally changed result"
    );
    cleanup(&r);
}
#[test]
fn batch_execute_known_native_rejection_and_unknown_submission_never_continue() {
    for change in [Change::Rejected, Change::Unknown] {
        let root = fixture();
        let platform = Racing {
            root: root.path().into(),
            calls: Cell::new(0),
            change,
        };
        let r = perform(
            &platform,
            &request(root.path(), &[("a", "new"), ("b", "other")]),
        );
        assert_eq!(
            r.status,
            if matches!(platform.change, Change::Unknown) {
                RelocationStatus::Uncertain
            } else {
                RelocationStatus::NotStarted
            }
        );
        assert_eq!(platform.calls.get(), 1);
        assert!(r.items[1].operation_id.is_none());
        assert_eq!(fs::read(root.path().join("a")).unwrap(), b"OWNED a\0");
        assert_eq!(fs::read(root.path().join("b")).unwrap(), b"OWNED b\0");
        cleanup(&r);
    }
}
#[test]
fn batch_execute_checkpoint_failure_after_first_child_keeps_partial_evidence_and_stops() {
    let root = fixture();
    let mut receipt = prepare(&request(root.path(), &[("a", "new"), ("b", "other")]));
    let result = execution::execute(
        &MacFiles,
        &mut receipt,
        &mut |r| {
            if r.items[0].completion_verified {
                Err(FileError::new(
                    FileErrorCode::Internal,
                    "injected aggregate checkpoint failure",
                ))
            } else {
                Ok(())
            }
        },
        &mut |i, req, anchor, batch| invoke(&MacFiles, i, req, anchor, batch),
    );
    receipt.record_error(result.unwrap_err());
    save(&receipt).unwrap();
    assert_eq!(receipt.status, RelocationStatus::Incomplete);
    assert!(receipt.items[0].completion_verified && receipt.items[1].operation_id.is_none());
    assert_eq!(fs::read(root.path().join("new")).unwrap(), b"OWNED a\0");
    assert_eq!(fs::read(root.path().join("b")).unwrap(), b"OWNED b\0");
    cleanup(&receipt);
}

#[test]
fn batch_execute_child_anchor_mismatch_refuses_native_submission_with_valid_source_guards() {
    for field in ["path", "identity", "version"] {
        let root = fixture();
        let mut receipt = prepare(&request(root.path(), &[("a", "new"), ("b", "other")]));
        let result = execution::execute(
            &MacFiles,
            &mut receipt,
            &mut save,
            &mut |i, req, anchor, batch| {
                let mut wrong = anchor.clone();
                match field {
                    "path" => wrong.path = "/foreign".into(),
                    "identity" => wrong.identity = "foreign".into(),
                    _ => wrong.version = "stale".into(),
                }
                invoke(&MacFiles, i, req, &wrong, batch)
            },
        );
        receipt.record_error(result.unwrap_err());
        save(&receipt).unwrap();
        assert_eq!(receipt.status, RelocationStatus::NotStarted);
        assert_eq!(
            receipt.items[0].error.as_ref().unwrap().code,
            FileErrorCode::Changed
        );
        assert!(receipt.items[1].operation_id.is_none());
        assert_eq!(fs::read(root.path().join("a")).unwrap(), b"OWNED a\0");
        assert_eq!(fs::read(root.path().join("b")).unwrap(), b"OWNED b\0");
        assert!(!root.path().join("new").exists());
        cleanup(&receipt);
    }
}

#[path = "batch_execution_lifetime_tests.rs"]
mod lifetime;

#[test]
fn batch_link_races_stop_on_object_changes_but_ignore_unselected_target_changes() {
    for change_kind in 0..3 {
        let root = tempfile::tempdir().unwrap();
        let outside = tempfile::tempdir().unwrap();
        let target = outside.path().join("PRIVATE");
        fs::write(&target, b"OWNED target").unwrap();
        symlink(&target, root.path().join("a")).unwrap();
        symlink("missing", root.path().join("b")).unwrap();
        let mut request = request(root.path(), &[("a", "new"), ("b", "other")]);
        for entry in &mut request.entries {
            entry.link_itself = true;
        }
        let platform = Racing {
            root: root.path().into(),
            calls: Cell::new(0),
            change: match change_kind {
                0 => Change::PendingLink,
                1 => Change::FinalLink,
                _ => Change::LinkTarget(target.clone()),
            },
        };
        let r = perform(&platform, &request);
        if change_kind == 2 {
            assert_eq!(r.status, RelocationStatus::Completed, "{:?}", r.error);
            assert!(r.completion_verified);
            assert_eq!(fs::read(&target).unwrap(), b"OWNED changed target");
            assert_eq!(fs::read_link(root.path().join("new")).unwrap(), target);
        } else {
            assert_eq!(r.status, RelocationStatus::Incomplete, "{:?}", r.error);
            assert!(!r.completion_verified && r.items[0].completion_verified);
            assert_eq!(fs::read(&target).unwrap(), b"OWNED target");
            if change_kind == 0 {
                assert_eq!(platform.calls.get(), 1);
                assert!(r.items[1].operation_id.is_none());
                assert_eq!(
                    fs::read_link(root.path().join("b")).unwrap(),
                    Path::new("replacement")
                );
                assert_eq!(
                    fs::read_link(root.path().join("saved-pending")).unwrap(),
                    Path::new("missing")
                );
            } else {
                assert_eq!(platform.calls.get(), 2);
                assert!(r.items[1].completion_verified);
                assert_eq!(
                    fs::read_link(root.path().join("saved-completed")).unwrap(),
                    target
                );
                assert_eq!(
                    fs::read_link(root.path().join("new")).unwrap(),
                    Path::new("replacement")
                );
            }
        }
        cleanup(&r);
    }
}