agentdir 0.1.0

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

fn create_test_tree(dir: &std::path::Path) {
    std::fs::write(dir.join("root.txt"), b"root content").unwrap();
    std::fs::write(dir.join("note.md"), b"markdown").unwrap();
    std::fs::create_dir(dir.join("nested")).unwrap();
    std::fs::write(dir.join("nested/child.txt"), b"child content").unwrap();
}

async fn mapped_workspace() -> (TempDir, TempDir, Workspace) {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    create_test_tree(src.path());

    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();

    (src, ws_dir, ws)
}

#[tokio::test]
async fn test_exists_returns_true_for_mapped_file() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;

    assert!(ws.exists(&VirtualPath::new("/docs/root.txt").unwrap()));
}

#[tokio::test]
async fn test_exists_returns_false_for_nonexistent() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;

    assert!(!ws.exists(&VirtualPath::new("/docs/missing.txt").unwrap()));
}

#[tokio::test]
async fn test_exists_returns_true_for_directory() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;

    assert!(ws.exists(&VirtualPath::new("/docs/nested").unwrap()));
}

#[tokio::test]
async fn test_stat_returns_metadata_for_file() {
    let (src, _ws_dir, ws) = mapped_workspace().await;
    let stat = ws
        .stat(&VirtualPath::new("/docs/root.txt").unwrap())
        .unwrap();
    let canonical_src = src.path().canonicalize().unwrap();

    assert_eq!(stat.virtual_path.as_str(), "/docs/root.txt");
    assert_eq!(stat.source_path.as_path(), canonical_src.join("root.txt"));
    assert_eq!(stat.size_bytes, 12);
    assert!(stat.mtime_ns > 0);
    assert_eq!(stat.entry_type, EntryType::File);
    assert!(stat.materialized);
}

#[tokio::test]
async fn test_stat_returns_metadata_for_directory() {
    let (src, _ws_dir, ws) = mapped_workspace().await;
    let stat = ws.stat(&VirtualPath::new("/docs/nested").unwrap()).unwrap();
    let canonical_src = src.path().canonicalize().unwrap();

    assert_eq!(stat.virtual_path.as_str(), "/docs/nested");
    assert_eq!(stat.source_path.as_path(), canonical_src.join("nested"));
    assert_eq!(stat.entry_type, EntryType::Directory);
    assert!(stat.materialized);
}

#[tokio::test]
async fn test_stat_errors_on_nonexistent() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let error = ws
        .stat(&VirtualPath::new("/docs/missing.txt").unwrap())
        .unwrap_err();

    assert!(matches!(error, AgentdirError::EntryNotFound(_)));
}

#[tokio::test]
async fn test_read_bytes_returns_source_content() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let bytes = ws
        .read_bytes(&VirtualPath::new("/docs/root.txt").unwrap())
        .await
        .unwrap();

    assert_eq!(bytes, b"root content");
}

#[tokio::test]
async fn test_read_bytes_reads_updated_source() {
    let (src, _ws_dir, ws) = mapped_workspace().await;
    std::fs::write(src.path().join("root.txt"), b"updated source").unwrap();

    let bytes = ws
        .read_bytes(&VirtualPath::new("/docs/root.txt").unwrap())
        .await
        .unwrap();

    assert_eq!(bytes, b"updated source");
}

#[tokio::test]
async fn test_read_bytes_errors_on_directory() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let error = ws
        .read_bytes(&VirtualPath::new("/docs/nested").unwrap())
        .await
        .unwrap_err();

    assert!(matches!(error, AgentdirError::InvalidPath(_)));
}

#[tokio::test]
async fn test_read_bytes_errors_on_nonexistent() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let error = ws
        .read_bytes(&VirtualPath::new("/docs/missing.txt").unwrap())
        .await
        .unwrap_err();

    assert!(matches!(error, AgentdirError::EntryNotFound(_)));
}

#[tokio::test]
async fn test_rglob_star_pattern() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let matches = ws.rglob("/docs/*.txt").unwrap();
    let paths: Vec<_> = matches
        .iter()
        .map(|entry| entry.virtual_path.as_str())
        .collect();

    assert_eq!(paths, vec!["/docs/root.txt"]);
}

#[tokio::test]
async fn test_rglob_recursive_doublestar() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let matches = ws.rglob("/docs/**/*.txt").unwrap();
    let mut paths: Vec<_> = matches
        .iter()
        .map(|entry| entry.virtual_path.as_str())
        .collect();
    paths.sort_unstable();

    assert_eq!(paths, vec!["/docs/nested/child.txt", "/docs/root.txt"]);
}

#[tokio::test]
async fn test_rglob_no_matches() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let matches = ws.rglob("/docs/**/*.rs").unwrap();

    assert!(matches.is_empty());
}

#[tokio::test]
async fn test_rglob_invalid_pattern_errors() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let result = ws.rglob("[invalid");
    assert!(result.is_err());
}

#[tokio::test]
async fn test_rglob_matches_directories() {
    let (_src, _ws_dir, ws) = mapped_workspace().await;
    let matches = ws.rglob("/docs/nested").unwrap();
    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].virtual_path.as_str(), "/docs/nested");
}