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::{FileErrorCode, FilePlatform};
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};

fn fixture() -> tempfile::TempDir {
    let root = tempfile::tempdir().unwrap();
    fs::create_dir(root.path().join("nested")).unwrap();
    for name in ["a", "b", "nested/c"] {
        fs::write(root.path().join(name), format!("OWNED {name}\0")).unwrap();
    }
    root
}
pub(super) fn request(root: &Path, proposals: &[(&str, &str)]) -> BatchRenameRequest {
    BatchRenameRequest {
        root: root.into(),
        entries: proposals
            .iter()
            .map(|(path, name)| {
                let parent = Path::new(path)
                    .parent()
                    .filter(|p| !p.as_os_str().is_empty())
                    .unwrap_or(Path::new("."));
                BatchRenameEntry {
                    path: path.into(),
                    name: (*name).into(),
                    expected_version: super::super::observe(root, Path::new(path), false, None)
                        .unwrap()
                        .version,
                    expected_parent_version: super::super::observe(root, parent, false, None)
                        .unwrap()
                        .version,
                    link_itself: false,
                }
            })
            .collect(),
    }
}
fn has(plan: &BatchRenamePlan, code: BatchRenameIssueCode, indices: &[usize]) -> bool {
    plan.issues
        .iter()
        .any(|issue| issue.code == code && issue.entries == indices)
}
#[test]
fn batch_rename_observes_order_exact_names_and_versions_without_writes() {
    let root = fixture();
    let request = request(
        root.path(),
        &[("a", "臺灣\n<external>"), ("nested/c", "c-copy")],
    );
    let before = fs::metadata(root.path().join("a")).unwrap();
    let plan = plan_worker(&request).unwrap();
    assert!(!plan.has_conflicts && !plan.execution_supported);
    assert!(plan.issues.is_empty());
    for (index, item) in plan.items.iter().enumerate() {
        assert_eq!(item.index, index);
        assert!(item.error.is_none());
        let proposal = item.proposal.as_ref().unwrap();
        assert_eq!(
            proposal.source.version,
            request.entries[index].expected_version
        );
        assert_eq!(
            proposal
                .request
                .destination
                .file_name()
                .unwrap()
                .to_str()
                .unwrap(),
            request.entries[index].name
        );
        assert!(!root.path().join(&proposal.request.destination).exists());
        assert!(root.path().join(&proposal.request.path).exists());
    }
    let after = fs::metadata(root.path().join("a")).unwrap();
    assert_eq!(
        (before.ino(), before.mode(), before.mtime(), before.ctime()),
        (after.ino(), after.mode(), after.mtime(), after.ctime())
    );
    assert_eq!(fs::read(root.path().join("a")).unwrap(), b"OWNED a\0");
}
#[test]
fn batch_rename_keeps_noops_and_existing_file_directory_broken_link_conflicts() {
    let root = fixture();
    fs::create_dir(root.path().join("existing-dir")).unwrap();
    symlink("missing-owned", root.path().join("broken")).unwrap();
    for name in ["b", "existing-dir", "broken"] {
        let plan = plan_worker(&request(root.path(), &[("a", name)])).unwrap();
        assert!(plan.has_conflicts);
        assert!(has(&plan, BatchRenameIssueCode::ExistingDestination, &[0]));
        assert!(
            plan.items[0]
                .proposal
                .as_ref()
                .unwrap()
                .destination_before
                .is_some()
        );
    }
    let plan = plan_worker(&request(root.path(), &[("a", "a")])).unwrap();
    assert!(!plan.has_conflicts && plan.items[0].proposal.as_ref().unwrap().no_op);
    assert_eq!(fs::read_dir(root.path()).unwrap().count(), 5);
}
#[test]
fn batch_rename_reports_duplicate_sources_destinations_and_shared_hardlink_identity() {
    let root = fixture();
    fs::hard_link(root.path().join("a"), root.path().join("link-a")).unwrap();
    for (entries, code) in [
        (
            vec![("a", "new"), ("a", "other")],
            BatchRenameIssueCode::DuplicateSource,
        ),
        (
            vec![("a", "new"), ("b", "new")],
            BatchRenameIssueCode::DuplicateDestination,
        ),
        (
            vec![("a", "new"), ("link-a", "other")],
            BatchRenameIssueCode::SharedSourceIdentity,
        ),
    ] {
        let plan = plan_worker(&request(root.path(), &entries)).unwrap();
        assert!(plan.has_conflicts && has(&plan, code, &[0, 1]));
    }
}
#[test]
fn batch_rename_conservatively_compares_case_and_canonical_unicode_only_in_same_parent() {
    let root = fixture();
    for (left, right) in [("Report", "report"), ("café", "café"), ("CAFÉ", "café")] {
        let plan = plan_worker(&request(root.path(), &[("a", left), ("b", right)])).unwrap();
        assert!(has(
            &plan,
            BatchRenameIssueCode::PotentialDestinationCollision,
            &[0, 1]
        ));
        assert_eq!(plan.request.entries[0].name, left);
        assert_eq!(plan.request.entries[1].name, right);
    }
    let plan = plan_worker(&request(
        root.path(),
        &[("a", "Report"), ("nested/c", "report")],
    ))
    .unwrap();
    assert!(!plan.has_conflicts);
    assert!(!MacFiles.names_may_collide("a", "á"));
}
#[test]
fn batch_rename_reports_swaps_chains_and_nested_directory_sources_without_ordering() {
    let root = fixture();
    for entries in [vec![("a", "b"), ("b", "a")], vec![("a", "b"), ("b", "new")]] {
        let plan = plan_worker(&request(root.path(), &entries)).unwrap();
        assert!(has(
            &plan,
            BatchRenameIssueCode::SourceDestinationDependency,
            &[0, 1]
        ));
        assert!(has(&plan, BatchRenameIssueCode::ExistingDestination, &[0]));
    }
    let plan = plan_worker(&request(
        root.path(),
        &[("nested", "renamed"), ("nested/c", "new")],
    ))
    .unwrap();
    assert!(has(&plan, BatchRenameIssueCode::NestedSource, &[0, 1]));
    assert!(root.path().join("nested/c").exists());
    assert!(!root.path().join("renamed").exists());
}
#[test]
fn batch_rename_item_errors_preserve_indices_and_do_not_hide_valid_proposals() {
    let root = fixture();
    symlink("a", root.path().join("source-link")).unwrap();
    let mut request = request(
        root.path(),
        &[
            ("a", "new"),
            ("source-link", "other"),
            ("b", "../bad"),
            ("nested/c", "new"),
        ],
    );
    request.entries[3].expected_version = "stale".into();
    let plan = plan_worker(&request).unwrap();
    assert!(plan.has_conflicts && !plan.execution_supported);
    assert_eq!(plan.items.len(), 4);
    assert!(plan.items[0].proposal.is_some());
    for (index, code) in [
        (1, FileErrorCode::SymlinkDisallowed),
        (2, FileErrorCode::InvalidOptions),
        (3, FileErrorCode::Changed),
    ] {
        assert_eq!(plan.items[index].index, index);
        assert_eq!(plan.items[index].error.as_ref().unwrap().code, code);
        assert!(plan.items[index].proposal.is_none());
    }
    assert!(root.path().join("a").exists());
    assert!(!root.path().join("new").exists());
}
#[test]
fn batch_rename_rejects_invalid_scope_count_and_worker_payload_budgets() {
    let root = fixture();
    let original = request(root.path(), &[("a", "new")]);
    for count in [0, 65] {
        let mut request = original.clone();
        request.entries = vec![request.entries[0].clone(); count];
        assert_eq!(
            plan_worker(&request).unwrap_err().code,
            FileErrorCode::InvalidOptions
        );
    }
    let mut request = original.clone();
    request.entries[0].name = "x".repeat(16384);
    assert_eq!(
        plan_worker(&request).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
    request = original;
    request.root = "relative".into();
    assert_eq!(
        plan_worker(&request).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
}
#[test]
fn batch_rename_timeout_stops_readonly_worker_without_names_changing() {
    let root = fixture();
    let executable = root.path().join("blocking-worker");
    fs::write(&executable, "#!/bin/sh\nexec /bin/sleep 30\n").unwrap();
    fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
    let request = request(root.path(), &[("a", "new")]);
    assert_eq!(
        run_plan(&executable, &request, 100).unwrap_err().code,
        FileErrorCode::Timeout
    );
    assert!(root.path().join("a").exists());
    assert!(!root.path().join("new").exists());
}

#[path = "batch_rename_safety_tests.rs"]
mod safety;
#[path = "batch_rename_root_tests.rs"]
mod roots;

#[test]
fn batch_rename_core_accepts_count_boundary_and_retains_all_failed_items() {
    let root = fixture();
    let mut request = request(root.path(), &[("a", "new")]);
    request.entries[0].path = "missing".into();
    request.entries = vec![request.entries[0].clone(); batch::MAX_BATCH_RENAMES];
    let plan = batch::plan(&MacFiles, &request).unwrap();
    assert_eq!(plan.items.len(), 64);
    assert!(plan.has_conflicts && !plan.execution_supported);
    assert_eq!(plan.items[63].index, 63);
    assert!(
        plan.items
            .iter()
            .all(|item| item.error.as_ref().unwrap().code == FileErrorCode::NotFound)
    );
    request.entries.push(request.entries[0].clone());
    assert_eq!(
        batch::plan(&MacFiles, &request).unwrap_err().code,
        FileErrorCode::InvalidOptions
    );
}

#[test]
fn batch_rename_packages_and_directories_are_observed_without_inspecting_contents() {
    let root = fixture();
    fs::create_dir(root.path().join("Owned.app")).unwrap();
    fs::write(
        root.path().join("Owned.app/payload"),
        b"OWNED package payload",
    )
    .unwrap();
    let plan = plan_worker(&request(
        root.path(),
        &[("Owned.app", "New.app"), ("nested", "new-nested")],
    ))
    .unwrap();
    assert!(!plan.has_conflicts);
    assert_eq!(
        plan.items[0].proposal.as_ref().unwrap().source.kind,
        cueward_core::files::FileKind::Directory
    );
    assert!(root.path().join("Owned.app/payload").exists());
    assert!(!root.path().join("New.app").exists());
}