bamboo-server 2026.4.27

HTTP server and API layer for the Bamboo agent framework
Documentation
use super::path::{should_skip_entry, to_display_name};
use actix_web::{body::to_bytes, http::StatusCode, web};
use std::path::Path;
use tempfile::tempdir;

#[test]
fn should_skip_entry_respects_hidden_flag() {
    assert!(should_skip_entry(".env", false, false));
    assert!(!should_skip_entry(".env", false, true));
}

#[test]
fn should_skip_entry_filters_known_build_dirs() {
    assert!(should_skip_entry("node_modules", true, true));
    assert!(should_skip_entry(".git", true, true));
    assert!(!should_skip_entry("src", true, true));
}

#[test]
fn to_display_name_returns_relative_path_when_possible() {
    let root = Path::new("/tmp/project");
    let nested = Path::new("/tmp/project/src/main.rs");
    assert_eq!(to_display_name(root, nested), "src/main.rs");
}

#[actix_web::test]
async fn workspace_config_registers_expected_routes() {
    let app = actix_web::test::init_service(actix_web::App::new().configure(super::config)).await;

    let requests = [
        actix_web::test::TestRequest::post()
            .uri("/workspace/validate")
            .to_request(),
        actix_web::test::TestRequest::get()
            .uri("/workspace/recent")
            .to_request(),
        actix_web::test::TestRequest::post()
            .uri("/workspace/recent")
            .to_request(),
        actix_web::test::TestRequest::get()
            .uri("/workspace/suggestions")
            .to_request(),
        actix_web::test::TestRequest::post()
            .uri("/workspace/browse-folder")
            .to_request(),
        actix_web::test::TestRequest::post()
            .uri("/workspace/files")
            .to_request(),
    ];

    for request in requests {
        let response = actix_web::test::call_service(&app, request).await;
        assert_ne!(
            response.status(),
            StatusCode::NOT_FOUND,
            "expected workspace route to be registered"
        );
    }
}

#[actix_web::test]
async fn browse_folder_returns_not_found_when_path_is_a_file() {
    let temp = tempdir().expect("create temp dir");
    let file_path = temp.path().join("README.md");
    std::fs::write(&file_path, "hello").expect("write test file");

    let app_state = web::Data::new(
        crate::app_state::AppState::new(temp.path().to_path_buf())
            .await
            .expect("app state should initialize"),
    );
    let app = actix_web::test::init_service(
        actix_web::App::new()
            .app_data(app_state)
            .configure(super::config),
    )
    .await;

    let req = actix_web::test::TestRequest::post()
        .uri("/workspace/browse-folder")
        .set_json(serde_json::json!({ "path": file_path.to_string_lossy().to_string() }))
        .to_request();
    let resp = actix_web::test::call_service(&app, req).await;

    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[actix_web::test]
async fn browse_folder_returns_only_directories_sorted_case_insensitively() {
    let temp = tempdir().expect("create temp dir");
    let workspace_root = temp.path().join("ws");
    std::fs::create_dir_all(&workspace_root).expect("create workspace root");
    let alpha = workspace_root.join("Alpha");
    let zeta = workspace_root.join("zeta");
    std::fs::create_dir_all(&alpha).expect("create alpha dir");
    std::fs::create_dir_all(&zeta).expect("create zeta dir");
    std::fs::write(workspace_root.join("notes.txt"), "ignore file").expect("write plain file");

    let app_state = web::Data::new(
        crate::app_state::AppState::new(temp.path().to_path_buf())
            .await
            .expect("app state should initialize"),
    );
    let app = actix_web::test::init_service(
        actix_web::App::new()
            .app_data(app_state)
            .configure(super::config),
    )
    .await;

    let req = actix_web::test::TestRequest::post()
        .uri("/workspace/browse-folder")
        .set_json(serde_json::json!({ "path": workspace_root.to_string_lossy().to_string() }))
        .to_request();
    let resp = actix_web::test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);

    let body = to_bytes(resp.into_body())
        .await
        .expect("read response body");
    let payload: serde_json::Value = serde_json::from_slice(&body).expect("parse browse payload");
    let folder_names = payload["folders"]
        .as_array()
        .expect("folders should be an array")
        .iter()
        .map(|item| item["name"].as_str().unwrap_or_default().to_string())
        .collect::<Vec<_>>();

    assert_eq!(folder_names, vec!["Alpha".to_string(), "zeta".to_string()]);
}

#[actix_web::test]
async fn list_workspace_files_respects_depth_and_hidden_flag() {
    let temp = tempdir().expect("create temp dir");
    let workspace_root = temp.path().join("ws");
    std::fs::create_dir_all(&workspace_root).expect("create workspace root");
    std::fs::write(workspace_root.join("visible.txt"), "v").expect("write visible file");
    std::fs::write(workspace_root.join(".hidden.txt"), "h").expect("write hidden file");
    let nested = workspace_root.join("nested");
    std::fs::create_dir_all(&nested).expect("create nested dir");
    std::fs::write(nested.join("deep.txt"), "d").expect("write deep file");

    let app_state = web::Data::new(
        crate::app_state::AppState::new(temp.path().to_path_buf())
            .await
            .expect("app state should initialize"),
    );
    let app = actix_web::test::init_service(
        actix_web::App::new()
            .app_data(app_state)
            .configure(super::config),
    )
    .await;

    let req = actix_web::test::TestRequest::post()
        .uri("/workspace/files")
        .set_json(serde_json::json!({
            "path": workspace_root.to_string_lossy().to_string(),
            "max_depth": 0,
            "include_hidden": false
        }))
        .to_request();
    let resp = actix_web::test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
    let body = to_bytes(resp.into_body())
        .await
        .expect("read response body");
    let payload: serde_json::Value =
        serde_json::from_slice(&body).expect("parse workspace files payload");
    let names = payload
        .as_array()
        .expect("workspace files should be an array")
        .iter()
        .map(|entry| entry["name"].as_str().unwrap_or_default().to_string())
        .collect::<Vec<_>>();

    assert_eq!(names, vec!["visible.txt".to_string()]);
}

#[actix_web::test]
async fn list_workspace_files_returns_not_found_for_missing_workspace() {
    let temp = tempdir().expect("create temp dir");
    let missing_path = temp.path().join("does-not-exist");

    let app_state = web::Data::new(
        crate::app_state::AppState::new(temp.path().to_path_buf())
            .await
            .expect("app state should initialize"),
    );
    let app = actix_web::test::init_service(
        actix_web::App::new()
            .app_data(app_state)
            .configure(super::config),
    )
    .await;

    let req = actix_web::test::TestRequest::post()
        .uri("/workspace/files")
        .set_json(serde_json::json!({ "path": missing_path.to_string_lossy().to_string() }))
        .to_request();
    let resp = actix_web::test::call_service(&app, req).await;

    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}