agentdir 0.1.2

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

fn create_source_files(dir: &std::path::Path) {
    std::fs::write(dir.join("a.txt"), b"aaa").unwrap();
    std::fs::write(dir.join("b.txt"), b"bbb").unwrap();
    std::fs::write(dir.join("c.txt"), b"ccc").unwrap();
}

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

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let summary = ws
        .map_batch(vec![(
            SourcePath::new(canonical_src.join("file.txt")),
            VirtualPath::new("/organized/file.txt").unwrap(),
        )])
        .await
        .unwrap();

    assert_eq!(summary.entries_added, 1);
    assert!(ws.exists(&VirtualPath::new("/organized/file.txt").unwrap()));
    assert_eq!(
        std::fs::read(ws_dir.path().join("organized/file.txt")).unwrap(),
        b"content"
    );
}

#[tokio::test]
async fn test_batch_map_multiple_files() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    create_source_files(src.path());
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let mappings = vec![
        (
            SourcePath::new(canonical_src.join("a.txt")),
            VirtualPath::new("/docs/alpha.txt").unwrap(),
        ),
        (
            SourcePath::new(canonical_src.join("b.txt")),
            VirtualPath::new("/docs/beta.txt").unwrap(),
        ),
        (
            SourcePath::new(canonical_src.join("c.txt")),
            VirtualPath::new("/other/gamma.txt").unwrap(),
        ),
    ];

    let summary = ws.map_batch(mappings).await.unwrap();
    assert_eq!(summary.entries_added, 3);
    assert!(summary.errors.is_empty());
    assert_eq!(ws.catalog.len(), 3);
}

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

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    ws.map_batch(vec![(
        SourcePath::new(canonical_src.join("file.txt")),
        VirtualPath::new("/deep/nested/path/file.txt").unwrap(),
    )])
    .await
    .unwrap();

    assert!(ws_dir.path().join("deep/nested/path/file.txt").exists());
}

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

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let result = ws
        .map_batch(vec![(
            SourcePath::new(std::path::PathBuf::from("/nonexistent/file.txt")),
            VirtualPath::new("/docs/file.txt").unwrap(),
        )])
        .await;

    assert!(result.is_err());
}

#[tokio::test]
async fn test_batch_map_rejects_duplicate_virtual() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("a.txt"), b"aaa").unwrap();
    std::fs::write(src.path().join("b.txt"), b"bbb").unwrap();
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let result = ws
        .map_batch(vec![
            (
                SourcePath::new(canonical_src.join("a.txt")),
                VirtualPath::new("/docs/same.txt").unwrap(),
            ),
            (
                SourcePath::new(canonical_src.join("b.txt")),
                VirtualPath::new("/docs/same.txt").unwrap(),
            ),
        ])
        .await;

    assert!(result.is_err());
}

#[tokio::test]
async fn test_batch_map_rejects_existing_virtual() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("a.txt"), b"aaa").unwrap();
    std::fs::write(src.path().join("b.txt"), b"bbb").unwrap();
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    ws.map_batch(vec![(
        SourcePath::new(canonical_src.join("a.txt")),
        VirtualPath::new("/docs/existing.txt").unwrap(),
    )])
    .await
    .unwrap();

    let result = ws
        .map_batch(vec![(
            SourcePath::new(canonical_src.join("b.txt")),
            VirtualPath::new("/docs/existing.txt").unwrap(),
        )])
        .await;

    assert!(result.is_err());
}

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

    let summary = ws.map_batch(vec![]).await.unwrap();
    assert_eq!(summary.entries_added, 0);
    assert!(summary.errors.is_empty());
}

#[tokio::test]
async fn test_batch_map_preserves_existing_entries() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    create_source_files(src.path());
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    ws.map(
        SourcePath::new(canonical_src.clone()),
        VirtualPath::new("/existing").unwrap(),
    )
    .await
    .unwrap();
    let count_before = ws.catalog.len();

    ws.map_batch(vec![(
        SourcePath::new(canonical_src.join("a.txt")),
        VirtualPath::new("/new/a.txt").unwrap(),
    )])
    .await
    .unwrap();

    assert_eq!(ws.catalog.len(), count_before + 1);
    assert!(ws.exists(&VirtualPath::new("/existing/a.txt").unwrap()));
    assert!(ws.exists(&VirtualPath::new("/new/a.txt").unwrap()));
}

#[tokio::test]
async fn test_batch_map_then_export() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    create_source_files(src.path());
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    ws.map_batch(vec![
        (
            SourcePath::new(canonical_src.join("a.txt")),
            VirtualPath::new("/organized/alpha.txt").unwrap(),
        ),
        (
            SourcePath::new(canonical_src.join("b.txt")),
            VirtualPath::new("/organized/beta.txt").unwrap(),
        ),
    ])
    .await
    .unwrap();

    let mapping = ws
        .export_mapping(MappingDirection::VirtualToSource, None)
        .unwrap();
    assert_eq!(mapping.len(), 2);
    assert!(mapping.contains_key("/organized/alpha.txt"));
    assert!(mapping.contains_key("/organized/beta.txt"));
}

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

    {
        let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
        ws.map_batch(vec![(
            SourcePath::new(canonical_src.join("file.txt")),
            VirtualPath::new("/docs/file.txt").unwrap(),
        )])
        .await
        .unwrap();
    }

    let ws = Workspace::open(ws_dir.path().to_path_buf()).unwrap();
    assert_eq!(ws.catalog.len(), 1);
    assert!(ws.exists(&VirtualPath::new("/docs/file.txt").unwrap()));
}

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

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let result = ws
        .map_batch(vec![(
            SourcePath::new(canonical_src.join("file.txt")),
            VirtualPath::new("relative/path.txt").unwrap(),
        )])
        .await;

    assert!(result.is_err());
}

#[tokio::test]
async fn test_batch_map_rejects_directory_source() {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::create_dir(src.path().join("subdir")).unwrap();
    let canonical_src = src.path().canonicalize().unwrap();

    let mut ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    let result = ws
        .map_batch(vec![(
            SourcePath::new(canonical_src.join("subdir")),
            VirtualPath::new("/docs/subdir").unwrap(),
        )])
        .await;

    assert!(result.is_err());
}