agentdir 0.1.5

Virtual filesystem for agent-optimized file exploration using CoW reflinks
Documentation
//! File system edge cases: special filenames, binary content, symlinks, permissions, large files.

use agentdir::types::{SourcePath, VirtualPath};
use agentdir::workspace::Workspace;
use tempfile::TempDir;

fn map_source<'a>(
    ws: &'a mut Workspace,
    src: &'a TempDir,
    mount: &'static str,
) -> impl std::future::Future<Output = agentdir::error::Result<agentdir::workspace::MapSummary>> + 'a
{
    ws.map(
        SourcePath::new(src.path().to_path_buf()),
        VirtualPath::new(mount).unwrap(),
    )
}

fn vp(path: &str) -> VirtualPath {
    VirtualPath::new(path).unwrap()
}

fn sleep_for_mtime() {
    std::thread::sleep(std::time::Duration::from_millis(10));
}

#[tokio::test]
async fn test_empty_source_directory_maps_cleanly() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let summary = map_source(&mut ws, &src, "/empty").await.unwrap();

    assert!(summary.entries_added <= 1);
    assert!(ws.catalog.get(&vp("/empty")).is_ok() || ws.catalog.len() == 0);
    assert_eq!(summary.errors, 0);
}

#[tokio::test]
async fn test_source_dir_with_only_subdirs_no_files() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::create_dir_all(src.path().join("a/b/c")).unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    assert!(ws.catalog.get(&vp("/docs/a")).is_ok());
    assert!(ws.catalog.get(&vp("/docs/a/b")).is_ok());
    assert!(ws.catalog.get(&vp("/docs/a/b/c")).is_ok());
    assert!(ws_dir.path().join("docs/a/b/c").is_dir());
}

#[tokio::test]
async fn test_binary_file_content_preserved() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    let bytes: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
    std::fs::write(src.path().join("binary.bin"), &bytes).unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/files").await.unwrap();

    assert_eq!(
        std::fs::read(ws_dir.path().join("files/binary.bin")).unwrap(),
        bytes
    );
}

#[tokio::test]
async fn test_large_file_materialization() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    let bytes: Vec<u8> = (0..(10 * 1024 * 1024)).map(|i| (i % 251) as u8).collect();
    std::fs::write(src.path().join("large.bin"), bytes).unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/files").await.unwrap();

    let metadata = std::fs::metadata(ws_dir.path().join("files/large.bin")).unwrap();
    assert_eq!(metadata.len(), 10 * 1024 * 1024);
}

#[tokio::test]
async fn test_zero_byte_file() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("empty.txt"), b"").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    let path = ws_dir.path().join("docs/empty.txt");
    assert!(path.exists());
    assert_eq!(std::fs::metadata(path).unwrap().len(), 0);
}

#[tokio::test]
async fn test_filename_with_spaces() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("file with spaces.txt"), b"spaces").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/file with spaces.txt")).unwrap(),
        b"spaces"
    );
}

#[tokio::test]
async fn test_filename_with_unicode() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("日本語ファイル.txt"), b"unicode").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/日本語ファイル.txt")).unwrap(),
        b"unicode"
    );
}

#[tokio::test]
async fn test_deeply_nested_directory_tree() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    let nested = (b'a'..=b't')
        .map(|ch| (ch as char).to_string())
        .collect::<Vec<_>>()
        .join("/");
    std::fs::create_dir_all(src.path().join(&nested)).unwrap();
    std::fs::write(src.path().join(&nested).join("leaf.txt"), b"leaf").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/tree").await.unwrap();

    assert_eq!(
        std::fs::read(ws_dir.path().join("tree").join(&nested).join("leaf.txt")).unwrap(),
        b"leaf"
    );
}

#[cfg(unix)]
#[tokio::test]
async fn test_symlink_in_source_is_skipped() {
    use std::os::unix::fs::symlink as unix_symlink;

    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("real.txt"), b"real").unwrap();
    unix_symlink(src.path().join("real.txt"), src.path().join("link.txt")).unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    assert!(ws.catalog.get(&vp("/docs/real.txt")).is_ok());
    assert!(ws.catalog.get(&vp("/docs/link.txt")).is_err());
    assert!(!ws_dir.path().join("docs/link.txt").exists());
}

#[cfg(unix)]
#[tokio::test]
async fn test_source_becomes_unreadable_after_map() {
    use std::os::unix::fs::PermissionsExt;

    // Root bypasses file permissions, so this test is meaningless when running as
    // root (e.g., inside Docker containers). Detect by creating a 000 file and
    // checking if we can still read it.
    {
        let probe = std::env::temp_dir().join("agentdir_root_probe");
        std::fs::write(&probe, b"x").unwrap();
        std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o000)).unwrap();
        let can_read = std::fs::read(&probe).is_ok();
        std::fs::set_permissions(&probe, std::fs::Permissions::from_mode(0o644)).unwrap();
        let _ = std::fs::remove_file(&probe);
        if can_read {
            eprintln!("SKIPPED: running as root (permissions not enforced)");
            return;
        }
    }

    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    let locked = src.path().join("locked.txt");
    std::fs::write(&locked, b"readable").unwrap();
    std::fs::write(src.path().join("other.txt"), b"other").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    sleep_for_mtime();
    std::fs::write(&locked, b"changed").unwrap();
    let original_permissions = std::fs::metadata(&locked).unwrap().permissions();
    let mut unreadable_permissions = original_permissions.clone();
    unreadable_permissions.set_mode(0o000);
    std::fs::set_permissions(&locked, unreadable_permissions).unwrap();

    let summary = ws.refresh().await.unwrap();

    std::fs::set_permissions(&locked, original_permissions).unwrap();

    assert!(!summary.errors.is_empty());
    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/other.txt")).unwrap(),
        b"other"
    );
}

#[tokio::test]
async fn test_materialized_file_manually_deleted() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("file.txt"), b"original").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    std::fs::remove_file(ws_dir.path().join("docs/file.txt")).unwrap();
    sleep_for_mtime();
    std::fs::write(src.path().join("file.txt"), b"modified").unwrap();

    let summary = ws.refresh().await.unwrap();

    assert!(summary.refreshed >= 1);
    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/file.txt")).unwrap(),
        b"modified"
    );
}

#[tokio::test]
async fn test_source_directory_deleted_entirely() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join("file.txt"), b"content").unwrap();
    std::fs::create_dir(src.path().join("subdir")).unwrap();
    std::fs::write(src.path().join("subdir/nested.txt"), b"nested").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    std::fs::remove_dir_all(src.path()).unwrap();

    let summary = ws.refresh().await.unwrap();

    assert!(summary.removed >= 2);
    assert_eq!(ws.catalog.len(), 0);
    assert!(!ws_dir.path().join("docs/file.txt").exists());
}

#[tokio::test]
async fn test_filename_with_special_chars() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    for name in ["file#1.txt", "file@2.txt", "file(3).txt"] {
        std::fs::write(src.path().join(name), name.as_bytes()).unwrap();
    }

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    for name in ["file#1.txt", "file@2.txt", "file(3).txt"] {
        assert_eq!(
            std::fs::read(ws_dir.path().join("docs").join(name)).unwrap(),
            name.as_bytes()
        );
    }
}

#[tokio::test]
async fn test_dot_files_and_dot_dirs() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();

    std::fs::write(src.path().join(".hidden"), b"hidden").unwrap();
    std::fs::create_dir(src.path().join(".config")).unwrap();
    std::fs::write(src.path().join(".config/settings.json"), b"{}").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    map_source(&mut ws, &src, "/docs").await.unwrap();

    assert!(ws.catalog.get(&vp("/docs/.hidden")).is_ok());
    assert!(ws.catalog.get(&vp("/docs/.config/settings.json")).is_ok());
    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/.hidden")).unwrap(),
        b"hidden"
    );
    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/.config/settings.json")).unwrap(),
        b"{}"
    );
}