vtcode-core 0.98.4

Core library for VT Code - a Rust-based terminal coding agent
use assert_fs::TempDir;
use serde_json::json;
use std::sync::Arc;
use vtcode_core::tools::file_ops::FileOpsTool;
use vtcode_core::tools::grep_file::GrepSearchManager;

#[tokio::test]
async fn create_file_rejects_workspace_escape() {
    let workspace = TempDir::new().expect("temp workspace");
    let outside = workspace
        .path()
        .parent()
        .expect("temp dir parent")
        .join("outside.txt");
    if outside.exists() {
        let _ = tokio::fs::remove_file(&outside).await;
    }

    let grep_manager = Arc::new(GrepSearchManager::new(workspace.path().to_path_buf()));
    let file_tool = FileOpsTool::new(workspace.path().to_path_buf(), grep_manager);

    let args = json!({
        "path": "../outside.txt",
        "content": "blocked"
    });
    let value = file_tool
        .create_file(args)
        .await
        .expect_err("tool should reject escapes");

    let message = value.to_string();
    assert!(
        message.contains("outside the workspace"),
        "expected workspace guard in error, got: {}",
        message
    );
    assert!(
        !outside.exists(),
        "create_file must not materialize escaped paths"
    );
}

#[tokio::test]
async fn move_file_rejects_workspace_escape_source() {
    let workspace = TempDir::new().expect("temp workspace");
    let outside = workspace
        .path()
        .parent()
        .expect("temp dir parent")
        .join("outside-move-source.txt");
    tokio::fs::write(&outside, "keep-me")
        .await
        .expect("outside source file should be created");

    let grep_manager = Arc::new(GrepSearchManager::new(workspace.path().to_path_buf()));
    let file_tool = FileOpsTool::new(workspace.path().to_path_buf(), grep_manager);

    let args = json!({
        "path": "../outside-move-source.txt",
        "destination": "moved.txt"
    });
    let error = file_tool
        .move_file(args)
        .await
        .expect_err("move_file should reject source path traversal")
        .to_string();

    assert!(error.contains("outside the workspace"));
    assert!(
        outside.exists(),
        "source file outside workspace should remain untouched"
    );
    assert!(
        !workspace.path().join("moved.txt").exists(),
        "destination should not be created for blocked move"
    );
}

#[tokio::test]
async fn copy_file_rejects_workspace_escape_destination() {
    let workspace = TempDir::new().expect("temp workspace");
    let inside = workspace.path().join("inside-copy-source.txt");
    tokio::fs::write(&inside, "copy-me")
        .await
        .expect("inside source file should be created");
    let outside = workspace
        .path()
        .parent()
        .expect("temp dir parent")
        .join("outside-copy-destination.txt");
    if outside.exists() {
        let _ = tokio::fs::remove_file(&outside).await;
    }

    let grep_manager = Arc::new(GrepSearchManager::new(workspace.path().to_path_buf()));
    let file_tool = FileOpsTool::new(workspace.path().to_path_buf(), grep_manager);

    let args = json!({
        "path": "inside-copy-source.txt",
        "destination": "../outside-copy-destination.txt"
    });
    let error = file_tool
        .copy_file(args)
        .await
        .expect_err("copy_file should reject destination path traversal")
        .to_string();

    assert!(error.contains("outside the workspace"));
    assert!(
        !outside.exists(),
        "destination outside workspace should not be created"
    );
    assert!(
        inside.exists(),
        "source file should remain in workspace after blocked copy"
    );
}

#[tokio::test]
async fn move_file_rejects_workspace_escape_destination() {
    let workspace = TempDir::new().expect("temp workspace");
    let source = workspace.path().join("inside-move-source.txt");
    tokio::fs::write(&source, "move-me")
        .await
        .expect("inside source file should be created");
    let outside = workspace
        .path()
        .parent()
        .expect("temp dir parent")
        .join("outside-move-destination.txt");
    if outside.exists() {
        let _ = tokio::fs::remove_file(&outside).await;
    }

    let grep_manager = Arc::new(GrepSearchManager::new(workspace.path().to_path_buf()));
    let file_tool = FileOpsTool::new(workspace.path().to_path_buf(), grep_manager);

    let args = json!({
        "path": "inside-move-source.txt",
        "destination": "../outside-move-destination.txt"
    });
    let error = file_tool
        .move_file(args)
        .await
        .expect_err("move_file should reject destination path traversal")
        .to_string();

    assert!(error.contains("outside the workspace"));
    assert!(
        !outside.exists(),
        "outside destination should not be created by blocked move"
    );
    assert!(
        source.exists(),
        "source should remain in workspace after blocked move"
    );
}

#[tokio::test]
async fn copy_file_rejects_workspace_escape_source() {
    let workspace = TempDir::new().expect("temp workspace");
    let outside = workspace
        .path()
        .parent()
        .expect("temp dir parent")
        .join("outside-copy-source.txt");
    tokio::fs::write(&outside, "copy-source")
        .await
        .expect("outside source file should be created");
    let inside_destination = workspace.path().join("inside-copy-destination.txt");
    if inside_destination.exists() {
        let _ = tokio::fs::remove_file(&inside_destination).await;
    }

    let grep_manager = Arc::new(GrepSearchManager::new(workspace.path().to_path_buf()));
    let file_tool = FileOpsTool::new(workspace.path().to_path_buf(), grep_manager);

    let args = json!({
        "path": "../outside-copy-source.txt",
        "destination": "inside-copy-destination.txt"
    });
    let error = file_tool
        .copy_file(args)
        .await
        .expect_err("copy_file should reject source path traversal")
        .to_string();

    assert!(error.contains("outside the workspace"));
    assert!(
        !inside_destination.exists(),
        "blocked copy should not create destination in workspace"
    );
    assert!(
        outside.exists(),
        "outside source file should remain untouched after blocked copy"
    );
}