scryer-engine 0.2.0

Tree-sitter and stack-graphs AST indexing engine for Scryer code intelligence
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;

use scryer_db::{Project, ScryerDb};
use scryer_engine::{ChangeDetector, EngineService, FileChange};

/// A file that is momentarily unreadable must not be reported as deleted: that would purge
/// its symbols and every cross-file edge pointing at it.
#[cfg(unix)]
#[tokio::test]
async fn test_unreadable_file_is_not_reported_deleted() -> anyhow::Result<()> {
    use std::os::unix::fs::PermissionsExt;

    let temp = tempdir()?;
    let root = temp.path();
    fs::create_dir_all(root.join("src"))?;
    fs::write(root.join("src/lib.rs"), "pub fn a() {}\n")?;
    fs::write(root.join("src/b.rs"), "pub fn b() {}\n")?;

    let db = ScryerDb::new_in_memory().await?;
    let project = {
        let mut conn = db.lock().await;
        Project::create()
            .slug("hasher-read-error".to_string())
            .root_path(root.to_string_lossy().to_string())
            .default_branch("main".to_string())
            .is_active(true)
            .created_at("2026-10-07T00:00:00Z".to_string())
            .updated_at("2026-10-07T00:00:00Z".to_string())
            .exec(&mut *conn)
            .await?
    };
    EngineService::new(db.clone())
        .index_project(project.id, root)
        .await?;

    let locked = root.join("src/b.rs");
    fs::set_permissions(&locked, fs::Permissions::from_mode(0o000))?;
    if fs::read(&locked).is_ok() {
        // Running as a user that ignores file modes (e.g. root): nothing to test.
        return Ok(());
    }

    let scanned = vec![PathBuf::from("src/lib.rs"), PathBuf::from("src/b.rs")];
    let report = ChangeDetector::detect_changes(&db, project.id, root, &scanned).await?;
    fs::set_permissions(&locked, fs::Permissions::from_mode(0o644))?;

    assert_eq!(report.deleted_count, 0, "{:?}", report.changes);
    assert!(
        !report
            .changes
            .iter()
            .any(|c| matches!(c, FileChange::Deleted { .. }))
    );
    Ok(())
}