dist_agent_lang 1.0.24

Agentic programming with library and CLI support for Off/On-chain network integration
Documentation
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use dist_agent_lang::ide::server::build_router;
use serde_json::{json, Value};
use std::fs;
use tempfile::TempDir;
use tower::ServiceExt;

fn setup_workspace() -> TempDir {
    let workspace = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(workspace.path().join("src")).expect("create src dir");
    fs::write(workspace.path().join("src/main.dal"), "print(\"ok\")").expect("write file");
    workspace
}

async fn post_json(app: axum::Router, uri: &str, body: Value) -> (StatusCode, Value) {
    let req = Request::builder()
        .method("POST")
        .uri(uri)
        .header("content-type", "application/json")
        .body(Body::from(body.to_string()))
        .expect("build request");

    let res = app.oneshot(req).await.expect("response");
    let status = res.status();
    let body_bytes = to_bytes(res.into_body(), usize::MAX)
        .await
        .expect("read body");
    let parsed = serde_json::from_slice::<Value>(&body_bytes).unwrap_or_else(|_| json!({}));
    (status, parsed)
}

fn canonical_string(path: &std::path::Path) -> String {
    path.canonicalize()
        .unwrap_or_else(|_| path.to_path_buf())
        .to_string_lossy()
        .to_string()
}

#[tokio::test]
async fn read_file_rejects_parent_traversal() {
    let workspace = setup_workspace();
    let app = build_router(workspace.path().to_path_buf());

    let (status, body) = post_json(
        app,
        "/api/agent/read_file",
        json!({
            "path": "../outside.txt"
        }),
    )
    .await;

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert!(body["error"]
        .as_str()
        .unwrap_or("")
        .contains("Invalid path"));
}

#[tokio::test]
async fn read_file_rejects_absolute_path_outside_workspace() {
    let workspace = setup_workspace();
    let outside = tempfile::NamedTempFile::new().expect("outside temp file");
    let app = build_router(workspace.path().to_path_buf());

    let (status, body) = post_json(
        app,
        "/api/agent/read_file",
        json!({
            "path": outside.path().to_string_lossy()
        }),
    )
    .await;

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert!(body["error"]
        .as_str()
        .unwrap_or("")
        .contains("Invalid path"));
}

#[tokio::test]
async fn write_file_allows_nonexistent_in_root_and_creates_parents() {
    let workspace = setup_workspace();
    let app = build_router(workspace.path().to_path_buf());

    let rel_path = "nested/new/file.txt";
    let target = workspace.path().join(rel_path);
    assert!(!target.exists());

    let (status, body) = post_json(
        app,
        "/api/agent/write_file",
        json!({
            "path": rel_path,
            "contents": "hello"
        }),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body["ok"], json!(true));
    assert_eq!(
        fs::read_to_string(&target).expect("read created file"),
        "hello"
    );
}

#[tokio::test]
async fn write_file_rejects_nonexistent_parent_traversal() {
    let workspace = setup_workspace();
    let app = build_router(workspace.path().to_path_buf());

    let (status, body) = post_json(
        app,
        "/api/agent/write_file",
        json!({
            "path": "../escape/new.txt",
            "contents": "nope"
        }),
    )
    .await;

    assert_eq!(status, StatusCode::BAD_REQUEST);
    assert!(body["error"]
        .as_str()
        .unwrap_or("")
        .contains("Invalid path"));
}

#[tokio::test]
async fn list_files_rejects_parent_traversal_subpath() {
    let workspace = setup_workspace();
    let app = build_router(workspace.path().to_path_buf());

    let req = Request::builder()
        .method("GET")
        .uri("/api/files?path=../")
        .body(Body::empty())
        .expect("build request");

    let res = app.oneshot(req).await.expect("response");
    assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}

#[cfg(unix)]
#[tokio::test]
async fn read_file_rejects_symlink_escape() {
    use std::os::unix::fs::symlink;

    let workspace = setup_workspace();
    let outside_dir = tempfile::tempdir().expect("outside dir");
    let outside_file = outside_dir.path().join("outside.txt");
    fs::write(&outside_file, "secret").expect("write outside file");

    let link_path = workspace.path().join("link_out");
    symlink(outside_dir.path(), &link_path).expect("create symlink");

    let app = build_router(workspace.path().to_path_buf());
    let (status, _body) = post_json(
        app,
        "/api/agent/read_file",
        json!({
            "path": "link_out/outside.txt"
        }),
    )
    .await;

    assert_ne!(status, StatusCode::OK);
    assert!(status.is_client_error());
}

#[tokio::test]
async fn read_file_allows_absolute_workspace_inside_root() {
    let workspace = setup_workspace();
    let nested_ws = workspace.path().join("nested_ws");
    fs::create_dir_all(nested_ws.join("sub")).expect("create nested workspace");
    fs::write(nested_ws.join("sub/file.txt"), "inside").expect("write nested file");

    let app = build_router(workspace.path().to_path_buf());
    let (status, body) = post_json(
        app,
        "/api/agent/read_file",
        json!({
            "workspace": nested_ws.to_string_lossy(),
            "path": "sub/file.txt"
        }),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body["ok"], json!(true));
    assert_eq!(body["contents"], json!("inside"));
}

#[tokio::test]
async fn read_file_workspace_outside_root_falls_back_to_server_root() {
    let workspace = setup_workspace();
    let outside = tempfile::tempdir().expect("outside workspace");
    fs::write(outside.path().join("src/main.dal"), "outside").ok();

    let app = build_router(workspace.path().to_path_buf());
    let (status, body) = post_json(
        app,
        "/api/agent/read_file",
        json!({
            "workspace": outside.path().to_string_lossy(),
            "path": "src/main.dal"
        }),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    assert_eq!(body["contents"], json!("print(\"ok\")"));
}

#[cfg(unix)]
#[tokio::test]
async fn run_command_outside_cwd_falls_back_to_workspace_root() {
    let workspace = setup_workspace();
    let outside = tempfile::tempdir().expect("outside cwd");
    let app = build_router(workspace.path().to_path_buf());

    let (status, body) = post_json(
        app,
        "/api/agent/run_command",
        json!({
            "cmd": "sh",
            "args": ["-c", "pwd"],
            "cwd": outside.path().to_string_lossy()
        }),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    let stdout = body["stdout"].as_str().unwrap_or("").trim();
    assert_eq!(stdout, canonical_string(workspace.path()));
}

#[cfg(unix)]
#[tokio::test]
async fn run_command_inside_cwd_uses_requested_workspace_subdir() {
    let workspace = setup_workspace();
    let inside = workspace.path().join("src");
    let app = build_router(workspace.path().to_path_buf());

    let (status, body) = post_json(
        app,
        "/api/agent/run_command",
        json!({
            "cmd": "sh",
            "args": ["-c", "pwd"],
            "cwd": inside.to_string_lossy()
        }),
    )
    .await;

    assert_eq!(status, StatusCode::OK);
    let stdout = body["stdout"].as_str().unwrap_or("").trim();
    assert_eq!(stdout, canonical_string(&inside));
}