agentdir 0.1.5

Virtual filesystem for agent-optimized file exploration using CoW reflinks
Documentation
use agentdir::types::{MaterializeStrategy, SourcePath, VirtualPath};
use agentdir::workspace::Workspace;
use tempfile::TempDir;

#[tokio::test]
async fn test_symlink_mode_creates_symlinks() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"hello symlink").unwrap();

    let mut ws =
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Symlink)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let mat = ws_dir.path().join("docs/file.txt");
    assert!(mat.symlink_metadata().unwrap().file_type().is_symlink());
}

#[tokio::test]
async fn test_symlink_mode_content_readable() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"readable").unwrap();

    let mut ws =
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Symlink)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

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

#[tokio::test]
async fn test_symlink_mode_source_update_visible() {
    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_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Symlink)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

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

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

#[tokio::test]
async fn test_symlink_mode_dematerialize() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"content").unwrap();

    let mut ws =
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Symlink)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    ws.unmap(&VirtualPath::new("/docs").unwrap()).unwrap();
    assert!(!ws_dir.path().join("docs/file.txt").exists());
}

#[tokio::test]
async fn test_virtual_mode_no_files_on_disk() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"virtual content").unwrap();

    let mut ws =
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Virtual)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    assert!(!ws_dir.path().join("docs/file.txt").exists());
    assert!(ws.exists(&VirtualPath::new("/docs/file.txt").unwrap()));
}

#[tokio::test]
async fn test_virtual_mode_read_bytes_works() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"backend read").unwrap();

    let mut ws =
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Virtual)
            .unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let bytes = ws
        .read_bytes(&VirtualPath::new("/docs/file.txt").unwrap())
        .await
        .unwrap();
    assert_eq!(bytes, b"backend read");
}

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

    {
        Workspace::init_with_strategy(ws_dir.path().to_path_buf(), MaterializeStrategy::Symlink)
            .unwrap();
    }

    let ws = Workspace::open(ws_dir.path().to_path_buf()).unwrap();
    assert_eq!(ws.catalog.manifest.strategy, MaterializeStrategy::Symlink);
}

#[tokio::test]
async fn test_default_strategy_is_reflink() {
    let ws_dir = TempDir::new().unwrap();
    let ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    assert_eq!(ws.catalog.manifest.strategy, MaterializeStrategy::Reflink);
}

#[tokio::test]
async fn test_legacy_manifest_defaults_to_reflink() {
    let ws_dir = TempDir::new().unwrap();
    Workspace::init(ws_dir.path().to_path_buf()).unwrap();

    let manifest_path = ws_dir.path().join(".agentdir/manifest.json");
    let json = std::fs::read_to_string(&manifest_path).unwrap();
    let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
    value.as_object_mut().unwrap().remove("strategy");
    std::fs::write(
        &manifest_path,
        serde_json::to_string_pretty(&value).unwrap(),
    )
    .unwrap();

    let ws = Workspace::open(ws_dir.path().to_path_buf()).unwrap();
    assert_eq!(ws.catalog.manifest.strategy, MaterializeStrategy::Reflink);
}

#[tokio::test]
async fn test_reflink_mode_unchanged() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file.txt"), b"reflink test").unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let mat = ws_dir.path().join("docs/file.txt");
    assert!(mat.exists());
    assert!(!mat.symlink_metadata().unwrap().file_type().is_symlink());
    assert_eq!(std::fs::read(&mat).unwrap(), b"reflink test");
}