roboticus-agent 0.10.0

Agent core with ReAct loop, policy engine, injection defense, memory system, and skill loader
Documentation
//! Exhaustive tests for `resolve_workspace_path_with_allowed()`.

use super::*;
use std::path::PathBuf;

fn tmp_workspace() -> (tempfile::TempDir, PathBuf) {
    let dir = tempfile::tempdir().unwrap();
    let root = std::fs::canonicalize(dir.path()).unwrap();
    (dir, root)
}

#[test]
fn relative_path_within_workspace_ok() {
    let (_dir, root) = tmp_workspace();
    std::fs::create_dir_all(root.join("src")).unwrap();
    std::fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
    let result = resolve_workspace_path_with_allowed(&root, "src/main.rs", false, &[]);
    assert!(result.is_ok(), "got: {:?}", result);
    assert!(result.unwrap().starts_with(&root));
}

#[test]
fn absolute_path_in_allowed_list_ok() {
    let allowed_dir = tempfile::tempdir().unwrap();
    let allowed = std::fs::canonicalize(allowed_dir.path()).unwrap();
    std::fs::write(allowed.join("data.txt"), "hello").unwrap();

    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(
        &root,
        allowed.join("data.txt").to_str().unwrap(),
        false,
        std::slice::from_ref(&allowed),
    );
    assert!(result.is_ok(), "got: {:?}", result);
}

#[test]
fn absolute_path_not_in_allowed_rejected() {
    let (_dir, root) = tmp_workspace();
    let outside = tempfile::tempdir().unwrap();
    let outside_path = std::fs::canonicalize(outside.path()).unwrap();
    std::fs::write(outside_path.join("secret.txt"), "nope").unwrap();

    let result = resolve_workspace_path_with_allowed(
        &root,
        outside_path.join("secret.txt").to_str().unwrap(),
        false,
        &[], // no allowed paths
    );
    assert!(result.is_err());
    let msg = result.unwrap_err().message;
    assert!(
        msg.contains("Sandbox boundary") || msg.contains("outside"),
        "unexpected error: {msg}"
    );
}

#[test]
fn path_traversal_blocked() {
    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(&root, "../../etc/passwd", false, &[]);
    assert!(result.is_err());
    assert!(result.unwrap_err().message.contains("path traversal"));
}

#[test]
fn allowed_path_nonexistent_with_flag() {
    let allowed_dir = tempfile::tempdir().unwrap();
    let allowed = std::fs::canonicalize(allowed_dir.path()).unwrap();
    // File does NOT exist
    let target = allowed.join("does_not_exist.txt");

    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(
        &root,
        target.to_str().unwrap(),
        true, // allow_nonexistent
        std::slice::from_ref(&allowed),
    );
    assert!(result.is_ok(), "got: {:?}", result);
}

#[test]
fn allowed_path_nonexistent_without_flag() {
    let allowed_dir = tempfile::tempdir().unwrap();
    let allowed = std::fs::canonicalize(allowed_dir.path()).unwrap();
    let target = allowed.join("does_not_exist.txt");

    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(
        &root,
        target.to_str().unwrap(),
        false, // allow_nonexistent = false
        std::slice::from_ref(&allowed),
    );
    assert!(result.is_err());
    assert!(result.unwrap_err().message.contains("does not exist"));
}

#[cfg(unix)]
#[test]
fn symlink_escape_blocked() {
    let (_dir, root) = tmp_workspace();
    let outside = tempfile::tempdir().unwrap();
    let outside_path = std::fs::canonicalize(outside.path()).unwrap();
    std::fs::write(outside_path.join("secret.txt"), "secret").unwrap();

    // Create symlink inside workspace pointing outside
    let link_path = root.join("sneaky_link");
    std::os::unix::fs::symlink(outside_path.join("secret.txt"), &link_path).unwrap();

    let result = resolve_workspace_path_with_allowed(&root, "sneaky_link", false, &[]);
    // After canonicalization, the real path is outside workspace → blocked
    assert!(result.is_err(), "symlink escape should be blocked");
}

#[test]
fn empty_allowed_paths_rejects_all_absolute() {
    let outside = tempfile::tempdir().unwrap();
    let outside_path = std::fs::canonicalize(outside.path()).unwrap();
    std::fs::write(outside_path.join("file.txt"), "data").unwrap();

    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(
        &root,
        outside_path.join("file.txt").to_str().unwrap(),
        false,
        &[], // empty
    );
    assert!(result.is_err());
}

#[test]
fn workspace_root_itself_ok() {
    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(&root, ".", false, &[]);
    assert!(result.is_ok(), "got: {:?}", result);
}

#[test]
fn dot_path_resolves_to_workspace() {
    let (_dir, root) = tmp_workspace();
    let resolved = resolve_workspace_path_with_allowed(&root, ".", false, &[]).unwrap();
    assert_eq!(
        std::fs::canonicalize(&resolved).unwrap(),
        std::fs::canonicalize(&root).unwrap()
    );
}

#[test]
fn multiple_allowed_paths_any_match() {
    let dir_a = tempfile::tempdir().unwrap();
    let dir_b = tempfile::tempdir().unwrap();
    let path_a = std::fs::canonicalize(dir_a.path()).unwrap();
    let path_b = std::fs::canonicalize(dir_b.path()).unwrap();
    std::fs::write(path_b.join("b_file.txt"), "in b").unwrap();

    let (_dir, root) = tmp_workspace();
    let result = resolve_workspace_path_with_allowed(
        &root,
        path_b.join("b_file.txt").to_str().unwrap(),
        false,
        &[path_a, path_b],
    );
    assert!(result.is_ok(), "got: {:?}", result);
}