rab-agent 0.1.0

rab is a lightweight, extensible, Rust-based coding agent.
Documentation
use rab::agent::extension::{Cancel, Extension};
use rab::builtin::bash::BashExtension;

fn tmp_dir() -> std::path::PathBuf {
    let d = std::env::temp_dir().join(format!("rab-test-{}", uuid::Uuid::new_v4()));
    std::fs::create_dir_all(&d).unwrap();
    d
}

#[tokio::test]
async fn runs_simple_command() {
    let tmp = tmp_dir();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let output = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "echo hello"}),
            Cancel::new(),
            None,
        )
        .await
        .unwrap();
    assert!(output.content.contains("hello"));
}

#[tokio::test]
async fn captures_stdout_and_stderr() {
    let tmp = tmp_dir();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let output = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "echo out && echo err >&2"}),
            Cancel::new(),
            None,
        )
        .await
        .unwrap();
    assert!(output.content.contains("out"));
}

#[tokio::test]
async fn runs_in_working_directory() {
    let tmp = tmp_dir();
    std::fs::write(tmp.join("marker.txt"), "present").unwrap();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let output = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "ls marker.txt"}),
            Cancel::new(),
            None,
        )
        .await
        .unwrap();
    assert!(output.content.contains("marker.txt"));
}

#[tokio::test]
async fn returns_error_on_nonzero_exit() {
    let tmp = tmp_dir();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let result = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "exit 1"}),
            Cancel::new(),
            None,
        )
        .await;
    assert!(result.is_err(), "non-zero exit should return error");
    let err = result.unwrap_err().to_string();
    assert!(err.contains("exit code") || err.contains("exit"));
}

#[tokio::test]
async fn handles_empty_output() {
    let tmp = tmp_dir();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let output = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "true"}),
            Cancel::new(),
            None,
        )
        .await
        .unwrap();
    assert!(!output.content.is_empty());
}

#[tokio::test]
async fn timeout_kills_command() {
    let tmp = tmp_dir();
    let ext = BashExtension::new(tmp.clone());
    let tools = ext.tools();
    let tool = &tools[0];

    let result = tool
        .execute(
            "id".into(),
            serde_json::json!({"command": "sleep 10", "timeout": 1}),
            Cancel::new(),
            None,
        )
        .await;
    assert!(result.is_err() || !result.as_ref().unwrap().content.is_empty());
}