agentdir 0.1.2

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

fn setup_base_workspace() -> (TempDir, TempDir, Workspace) {
    let src = TempDir::new().unwrap();
    let ws_dir = TempDir::new().unwrap();
    std::fs::write(src.path().join("file1.txt"), b"hello").unwrap();
    std::fs::write(src.path().join("file2.txt"), b"world").unwrap();
    let ws = Workspace::init(ws_dir.path().to_path_buf()).unwrap();
    (src, ws_dir, ws)
}

#[tokio::test]
async fn test_snapshot_creation() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let snap = ws.snapshot("run_001").unwrap();
    assert_eq!(snap.name, "run_001");
    assert!(snap.snapshot_root.exists());
}

#[tokio::test]
async fn test_snapshot_reads_base_files() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let snap = ws.snapshot("run_001").unwrap();
    let content = std::fs::read(
        snap.materializer
            .materialized_path(&VirtualPath::new("/docs/file1.txt").unwrap()),
    )
    .unwrap();
    assert_eq!(content, b"hello");
}

#[tokio::test]
async fn test_snapshot_write_is_local() {
    let (src, ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let mut snap = ws.snapshot("run_001").unwrap();
    snap.write(&VirtualPath::new("/output.txt").unwrap(), b"agent output")
        .unwrap();

    let snap_file = snap
        .materializer
        .materialized_path(&VirtualPath::new("/output.txt").unwrap());
    assert!(snap_file.exists());
    assert_eq!(std::fs::read(&snap_file).unwrap(), b"agent output");

    let base_file = ws_dir.path().join("output.txt");
    assert!(!base_file.exists());
}

#[tokio::test]
async fn test_snapshot_write_does_not_leak_to_base() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let mut snap = ws.snapshot("run_001").unwrap();
    snap.write(
        &VirtualPath::new("/docs/file1.txt").unwrap(),
        b"overwritten in snapshot",
    )
    .unwrap();

    let snap_content = std::fs::read(
        snap.materializer
            .materialized_path(&VirtualPath::new("/docs/file1.txt").unwrap()),
    )
    .unwrap();
    assert_eq!(snap_content, b"overwritten in snapshot");

    assert_eq!(
        std::fs::read(src.path().join("file1.txt")).unwrap(),
        b"hello"
    );
}

#[tokio::test]
async fn test_concurrent_snapshots_isolated() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let mut snap1 = ws.snapshot("run_001").unwrap();
    let mut snap2 = ws.snapshot("run_002").unwrap();

    snap1
        .write(&VirtualPath::new("/snap1_only.txt").unwrap(), b"snap1")
        .unwrap();
    snap2
        .write(&VirtualPath::new("/snap2_only.txt").unwrap(), b"snap2")
        .unwrap();

    assert!(snap1.exists(&VirtualPath::new("/snap1_only.txt").unwrap()));
    assert!(!snap1.exists(&VirtualPath::new("/snap2_only.txt").unwrap()));
    assert!(snap2.exists(&VirtualPath::new("/snap2_only.txt").unwrap()));
    assert!(!snap2.exists(&VirtualPath::new("/snap1_only.txt").unwrap()));
}

#[tokio::test]
async fn test_snapshot_destroy() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let snap = ws.snapshot("run_001").unwrap();
    let snap_root = snap.snapshot_root.clone();
    assert!(snap_root.exists());

    snap.destroy().unwrap();
    assert!(!snap_root.exists());
}

#[tokio::test]
async fn test_destroy_does_not_affect_base() {
    let (src, ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let snap = ws.snapshot("run_001").unwrap();
    snap.destroy().unwrap();

    assert!(ws_dir.path().join("docs/file1.txt").exists());
    assert_eq!(
        std::fs::read(ws_dir.path().join("docs/file1.txt")).unwrap(),
        b"hello"
    );
    assert!(ws.catalog.len() >= 2);
}

#[tokio::test]
async fn test_list_snapshots() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    assert!(ws.list_snapshots().unwrap().is_empty());

    let _s1 = ws.snapshot("beta").unwrap();
    let _s2 = ws.snapshot("alpha").unwrap();

    let names = ws.list_snapshots().unwrap();
    assert_eq!(names, vec!["alpha", "beta"]);
}

#[tokio::test]
async fn test_snapshot_duplicate_name_rejected() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let _snap = ws.snapshot("run_001").unwrap();
    let result = ws.snapshot("run_001");
    assert!(result.is_err());
}

#[tokio::test]
async fn test_destroy_snapshot_via_workspace() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    let _snap = ws.snapshot("run_001").unwrap();
    assert_eq!(ws.list_snapshots().unwrap().len(), 1);

    ws.destroy_snapshot("run_001").unwrap();
    assert!(ws.list_snapshots().unwrap().is_empty());
}

#[tokio::test]
async fn test_open_snapshot() {
    let (src, _ws_dir, mut ws) = setup_base_workspace();
    ws.map(
        SourcePath::new(src.path().canonicalize().unwrap()),
        VirtualPath::new("/docs").unwrap(),
    )
    .await
    .unwrap();

    {
        let mut snap = ws.snapshot("run_001").unwrap();
        snap.write(&VirtualPath::new("/output.txt").unwrap(), b"persisted")
            .unwrap();
    }

    let snap = ws.open_snapshot("run_001").unwrap();
    assert!(snap.exists(&VirtualPath::new("/output.txt").unwrap()));
    assert!(snap.exists(&VirtualPath::new("/docs/file1.txt").unwrap()));
}