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::cell::RefCell;
use std::fs;
use std::os::unix::fs::symlink;

#[derive(Default)]
struct Fixture {
    paths: Vec<PathBuf>,
    calls: RefCell<Vec<(PathBuf, String, usize)>>,
    error: Option<FileErrorCode>,
    mutate_root: bool,
}
impl Native for Fixture {
    fn search(&self, root: &Path, query: &str, max: usize) -> Result<Vec<PathBuf>, FileError> {
        self.calls
            .borrow_mut()
            .push((root.to_owned(), query.to_owned(), max));
        if self.mutate_root {
            fs::write(root.join("new-file"), "changed").unwrap();
        }
        if let Some(code) = self.error {
            return Err(FileError::new(code, "fixture"));
        }
        Ok(self.paths.clone())
    }
}

fn request(root: &Path) -> SpotlightRequest {
    SpotlightRequest {
        root: root.to_owned(),
        text: "臺灣 report".into(),
        max_depth: 1,
        hidden: false,
        max_candidates: 100,
        limit: 100,
    }
}
fn result(request: &SpotlightRequest, native: &impl Native) -> SpotlightResult {
    let SpotlightResponse::Spotlight(result) = execute(request, native).unwrap();
    result
}

#[test]
fn zero_index_candidates_never_claim_complete_filesystem_or_content_coverage() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("unindexed.txt"), "臺灣 report").unwrap();
    let fixture = Fixture::default();
    let req = request(dir.path());
    let output = result(&req, &fixture);
    assert!(output.entries.is_empty());
    assert!(output.query_completed);
    assert!(!output.enumeration_complete);
    assert!(!output.content_verified);
    assert!(matches!(output.index_coverage, IndexCoverage::Unknown));
    assert!(matches!(output.source, SpotlightSource::Spotlight));
    assert_eq!(
        fixture.calls.borrow()[0].0,
        dir.path().canonicalize().unwrap()
    );
    assert_eq!(fixture.calls.borrow()[0].2, 100);
    assert_eq!(
        fs::read_to_string(dir.path().join("unindexed.txt")).unwrap(),
        "臺灣 report"
    );
}

#[test]
fn sorted_bounded_results_preserve_unicode_newlines_and_metadata_without_content() {
    let dir = tempfile::tempdir().unwrap();
    let names = [
        "臺灣\n.txt",
        "a space.txt",
        "z.txt",
        "e\u{301}.txt",
        "é.txt",
    ];
    for name in names {
        fs::write(
            dir.path().join(name),
            "external </external> not matching query",
        )
        .unwrap();
    }
    let native = Fixture {
        paths: names
            .iter()
            .map(|name| dir.path().canonicalize().unwrap().join(name))
            .collect(),
        ..Default::default()
    };
    let mut req = request(dir.path());
    req.limit = 2;
    let output = result(&req, &native);
    assert_eq!(output.candidate_count, 5);
    assert_eq!(output.eligible_count, 5);
    assert_eq!(output.available_count, 5);
    assert_eq!(output.error_count, 0);
    assert!(output.truncated);
    assert_eq!(
        output
            .entries
            .iter()
            .map(SpotlightEntry::relative_path)
            .collect::<Vec<_>>(),
        ["a space.txt", "e\u{301}.txt"]
    );
    let json = serde_json::to_string(&output).unwrap();
    assert!(!json.contains("not matching query"));
    req.limit = 10;
    assert_eq!(result(&req, &native).entries.len(), 5);
}

#[test]
fn filters_exclude_outside_hidden_depth_duplicate_and_non_regular_candidates() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().canonicalize().unwrap();
    fs::create_dir(root.join("sub")).unwrap();
    fs::write(root.join("sub/deep"), "data").unwrap();
    fs::write(root.join(".hidden"), "data").unwrap();
    fs::write(root.join("visible"), "data").unwrap();
    let outside = tempfile::tempdir().unwrap();
    symlink(outside.path().join("missing"), root.join("leaf-link")).unwrap();
    let paths = [
        root.join("visible"),
        root.join("visible"),
        root.join(".hidden"),
        root.join("sub/deep"),
        root.join("sub"),
        root.join("leaf-link"),
        root.clone(),
        root.join("../outside"),
        outside.path().join("missing"),
        PathBuf::from(format!("{}-sibling/missing", root.display())),
    ];
    let native = Fixture {
        paths: paths.into(),
        ..Default::default()
    };
    let mut req = request(&root);
    let output = result(&req, &native);
    assert_eq!(output.entries.len(), 1);
    assert_eq!(output.excluded.outside_scope, 4);
    assert_eq!(output.excluded.hidden, 1);
    assert_eq!(output.excluded.depth, 1);
    assert_eq!(output.excluded.non_regular, 2);
    assert_eq!(output.excluded.duplicate, 1);
    req.hidden = true;
    req.max_depth = 2;
    let output = result(&req, &native);
    assert_eq!(output.entries.len(), 3);
    assert_eq!(output.excluded.hidden, 0);
    assert_eq!(output.excluded.depth, 0);
}

#[test]
fn stale_paths_and_ancestor_symlinks_remain_errors_instead_of_silent_zero_matches() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().canonicalize().unwrap();
    let outside = tempfile::tempdir().unwrap();
    fs::write(outside.path().join("private.txt"), "secret").unwrap();
    symlink(outside.path(), root.join("link")).unwrap();
    let native = Fixture {
        paths: vec![root.join("gone"), root.join("link/private.txt")],
        ..Default::default()
    };
    let mut req = request(&root);
    req.max_depth = 2;
    let output = result(&req, &native);
    assert_eq!(output.available_count, 0);
    assert_eq!(output.error_count, 2);
    assert_eq!(output.eligible_count, 2);
    assert!(
        matches!(&output.entries[0], SpotlightEntry::Error { error, .. } if error.code == FileErrorCode::NotFound)
    );
    assert!(
        matches!(&output.entries[1], SpotlightEntry::Error { error, .. } if error.code == FileErrorCode::SymlinkDisallowed)
    );
    assert!(!serde_json::to_string(&output).unwrap().contains("secret"));
}

#[test]
fn candidate_budget_applies_before_filters_and_never_returns_partial_success() {
    let dir = tempfile::tempdir().unwrap();
    let native = Fixture {
        paths: vec![PathBuf::from("/outside"), PathBuf::from("/another")],
        ..Default::default()
    };
    let mut req = request(dir.path());
    req.max_candidates = 1;
    req.limit = 1;
    assert_eq!(
        execute(&req, &native).unwrap_err().code,
        FileErrorCode::ScanLimit
    );
}

#[test]
fn root_changes_and_native_failures_discard_results() {
    let dir = tempfile::tempdir().unwrap();
    let req = request(dir.path());
    let native = Fixture {
        mutate_root: true,
        ..Default::default()
    };
    assert_eq!(
        execute(&req, &native).unwrap_err().code,
        FileErrorCode::Changed
    );
    for code in [
        FileErrorCode::PermissionDenied,
        FileErrorCode::Unavailable,
        FileErrorCode::Timeout,
    ] {
        let native = Fixture {
            error: Some(code),
            ..Default::default()
        };
        assert_eq!(execute(&req, &native).unwrap_err().code, code);
    }
}

#[test]
fn explicit_directory_root_is_required_before_native_query() {
    let dir = tempfile::tempdir().unwrap();
    let file = dir.path().join("file");
    fs::write(&file, "data").unwrap();
    let native = Fixture::default();
    for (root, code) in [
        (Path::new("relative"), FileErrorCode::InvalidOptions),
        (file.as_path(), FileErrorCode::UnsupportedType),
    ] {
        assert_eq!(execute(&request(root), &native).unwrap_err().code, code);
    }
    assert!(native.calls.borrow().is_empty());
}

#[test]
fn worker_requests_validate_all_budgets_and_reject_ambiguous_text() {
    let mut req = request(Path::new("/tmp"));
    for text in ["", " ", "wild*", "wild?", "new\nline", "nul\0", "tab\t"] {
        req.text = text.into();
        assert_eq!(
            expression(&req).unwrap_err().code,
            FileErrorCode::InvalidOptions
        );
    }
    req.text = "x".repeat(1025);
    assert!(expression(&req).is_err());
    req.text = "valid".into();
    for (depth, candidates, limit) in [
        (0, 1, 1),
        (33, 1, 1),
        (1, 0, 1),
        (1, 10001, 1),
        (1, 1, 0),
        (1, 1, 501),
    ] {
        req.max_depth = depth;
        req.max_candidates = candidates;
        req.limit = limit;
        assert_eq!(
            expression(&req).unwrap_err().code,
            FileErrorCode::InvalidOptions
        );
    }
}

#[test]
fn quotes_and_backslashes_cannot_escape_the_single_content_value() {
    let mut req = request(Path::new("/tmp"));
    req.text = "a\" || kMDItemFSName == \"b\\c".into();
    assert_eq!(
        expression(&req).unwrap(),
        "kMDItemTextContent == \"*a\\\" || kMDItemFSName == \\\"b\\\\c*\"c"
    );
}

#[test]
fn a_blocking_spotlight_worker_is_stopped_without_partial_success() {
    use std::os::unix::fs::PermissionsExt;
    let dir = tempfile::tempdir().unwrap();
    let exe = dir.path().join("worker");
    fs::write(&exe, "#!/bin/sh\nexec /bin/sleep 30\n").unwrap();
    fs::set_permissions(&exe, fs::Permissions::from_mode(0o700)).unwrap();
    let start = std::time::Instant::now();
    assert_eq!(
        run(&exe, &request(dir.path()), 100).unwrap_err().code,
        FileErrorCode::Timeout
    );
    assert!(start.elapsed() < std::time::Duration::from_secs(5));
}

#[test]
fn path_budgets_and_encoding_apply_before_scope_filters_or_deduplication() {
    use std::os::unix::ffi::OsStringExt;
    let dir = tempfile::tempdir().unwrap();
    let req = request(dir.path());
    for (path, code) in [
        (
            PathBuf::from(std::ffi::OsString::from_vec(vec![b'/', 0xff])),
            FileErrorCode::UnsupportedPathEncoding,
        ),
        (
            PathBuf::from("/nul\0path"),
            FileErrorCode::UnsupportedPathEncoding,
        ),
        (
            PathBuf::from(format!("/{}", "x".repeat(16384))),
            FileErrorCode::ScanLimit,
        ),
    ] {
        let native = Fixture {
            paths: vec![path],
            ..Default::default()
        };
        assert_eq!(execute(&req, &native).unwrap_err().code, code);
    }
    let mut req = req;
    req.max_candidates = 1000;
    let native = Fixture {
        paths: vec![PathBuf::from(format!("/{}", "x".repeat(16383))); 257],
        ..Default::default()
    };
    assert_eq!(
        execute(&req, &native).unwrap_err().code,
        FileErrorCode::ScanLimit
    );
}