magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[test]
fn bash_output_is_bounded() {
    let (_temp, runtime) = runtime();
    let stdout = runtime.dispatch(
        "bash",
        json!({"command": format!("yes x | head -c {}", BASH_STDOUT_MAX_BYTES + 4096)}),
    );
    assert!(!stdout.success);
    assert_eq!(
        stdout.metadata["stdout"].as_str().unwrap().len(),
        BASH_STDOUT_MAX_BYTES
    );
    assert_eq!(stdout.metadata["stdout_truncated"], true);
    assert_eq!(stdout.metadata["stdout_limit_bytes"], BASH_STDOUT_MAX_BYTES);
    assert!(stdout.content.contains("output truncated"));

    let stderr = runtime.dispatch(
        "bash",
        json!({"command": format!("yes err >&2 | head -c {}", BASH_STDERR_MAX_BYTES + 4096)}),
    );
    assert!(!stderr.success);
    assert_eq!(
        stderr.metadata["stderr"].as_str().unwrap().len(),
        BASH_STDERR_MAX_BYTES
    );
    assert_eq!(stderr.metadata["stderr_truncated"], true);
    assert_eq!(stderr.metadata["stderr_limit_bytes"], BASH_STDERR_MAX_BYTES);
}

#[test]
fn bash_captures_stdout_stderr_and_exit_code() {
    let (_temp, runtime) = runtime();
    let result = runtime.dispatch("bash", json!({"command":"echo out; echo err >&2; exit 7"}));
    assert!(!result.success);
    assert_eq!(result.metadata["exit_code"], 7);
    assert!(result.metadata["stdout"].as_str().unwrap().contains("out"));
    assert!(result.metadata["stderr"].as_str().unwrap().contains("err"));
}

#[cfg(windows)]
#[test]
fn bash_runs_powershell_stdout_stderr_and_exit_code() {
    let (_temp, runtime) = runtime();
    let result = runtime.dispatch(
        "bash",
        json!({"command":"Write-Output ok; Write-Error err; exit 7"}),
    );
    assert!(!result.success);
    assert_eq!(result.tool_name, "bash");
    assert_eq!(result.metadata["exit_code"], 7);
    assert!(result.metadata["stdout"].as_str().unwrap().contains("ok"));
    assert!(result.metadata["stderr"].as_str().unwrap().contains("err"));
}

#[test]
fn shell_alias_dispatches_to_bash_and_unknown_tool_reports_original_name() {
    let (_temp, runtime) = runtime();
    let result = runtime.dispatch("shell", json!({"command":"echo alias"}));
    assert!(result.success, "{}", result.content);
    assert_eq!(result.tool_name, "bash");
    assert!(
        result.metadata["stdout"]
            .as_str()
            .unwrap()
            .contains("alias")
    );

    let unknown = runtime.dispatch("nope", json!({}));
    assert!(!unknown.success);
    assert_eq!(unknown.tool_name, "nope");
    assert!(unknown.content.contains("unknown tool 'nope'"));
}

#[test]
fn mvp_tool_specs_define_provider_order_and_bash_alias() {
    assert_eq!(
        capability::MVP_TOOL_CAPABILITIES
            .iter()
            .map(|tool| tool.canonical_name())
            .collect::<Vec<_>>(),
        vec![
            "read",
            "view_image",
            "bash",
            "hash_edit",
            "write",
            "grep",
            "find",
            "list_files",
            "subagents",
            "web",
            "ast_grep"
        ]
    );
    assert_eq!(
        ToolCapability::from_dispatch_name("shell"),
        Some(ToolCapability::Bash)
    );
    assert_eq!(ToolCapability::from_dispatch_name("missing"), None);
    let definitions = mvp_tool_definitions_json();
    let definitions = definitions.as_array().unwrap();
    assert_eq!(definitions.len(), 11);
    assert!(
        !definitions
            .iter()
            .any(|definition| definition["name"] == "shell")
    );
    assert!(
        !definitions
            .iter()
            .any(|definition| definition["name"] == "cmd")
    );
    let read = definitions
        .iter()
        .find(|definition| definition["name"] == "read")
        .unwrap();
    assert!(read["parameters"]["properties"].get("path").is_none());
    assert!(read["parameters"]["properties"].get("paths").is_some());
    assert_eq!(read["parameters"]["required"], json!(["paths"]));
    assert_eq!(read["parameters"]["properties"]["paths"]["minItems"], 1);
    assert_eq!(
        read["parameters"]["properties"]["paths"]["maxItems"],
        FILE_READ_MAX_FILES
    );
    assert_eq!(read["parameters"]["additionalProperties"], false);
    assert!(read["parameters"].get("oneOf").is_none());
    assert!(read["parameters"].get("anyOf").is_none());
    assert!(read["parameters"].get("allOf").is_none());
    assert!(read["parameters"].get("enum").is_none());
    assert!(read["parameters"].get("not").is_none());
    assert_eq!(read["parameters"]["x-maxBytes"], FILE_READ_MAX_BYTES);
    assert_eq!(read["parameters"]["x-maxLines"], FILE_READ_MAX_LINES);
    assert_eq!(read["parameters"]["x-maxFiles"], FILE_READ_MAX_FILES);
    let read_description = read["description"].as_str().unwrap();
    assert_eq!(
        read_description,
        "**Tool Name**: `read`\nRead ordered local, URL, or internal text resources. Prefer focused local-file reads: provide `offset` and `limit`, or use a line selector, instead of reading an entire file. For local files without a selector, omitting `limit` reads up to 400 lines. Use public unauthenticated URLs when text, Markdown, or JSON is enough; this tool does not run JavaScript or support interaction. Local files return current-session `[PATH#TAG]` and 1-based `LINE:TEXT` anchors by default; `TAG` covers the whole file. For `hash_edit`, use only anchors returned by the latest non-raw read. Follow truncation and omission notices.",
    );
    assert_eq!(
        read_description
            .lines()
            .filter(|line| !line.is_empty())
            .count(),
        2
    );

    let find = definitions
        .iter()
        .find(|definition| definition["name"] == "find")
        .unwrap();
    assert_eq!(find["parameters"]["required"], json!(["query"]));
    assert_eq!(find["parameters"]["additionalProperties"], false);
    assert_eq!(find["parameters"]["properties"]["query"]["maxLength"], 512);
    assert_eq!(
        find["parameters"]["properties"]["kind"]["enum"],
        json!(["files", "directories", "mixed"])
    );
    assert_eq!(
        find["parameters"]["properties"]["limit"]["maximum"],
        crate::tools::args::FIND_MAX_LIMIT
    );
    assert!(
        find["description"]
            .as_str()
            .unwrap()
            .contains("**Tool Name**: `find`")
    );

    let bash = definitions
        .iter()
        .find(|definition| definition["name"] == "bash")
        .unwrap();
    assert_eq!(bash["parameters"]["properties"]["command"]["minLength"], 1);
    assert_eq!(
        bash["parameters"]["properties"]["command"]["description"],
        "Shell command to run."
    );
    assert_eq!(
        bash["parameters"]["properties"]["timeout"]["description"],
        "Timeout in seconds; defaults to 30."
    );
    assert!(bash["parameters"]["properties"].get("cmd").is_none());
    assert!(bash["parameters"].get("anyOf").is_none());
    assert_eq!(bash["parameters"]["required"], json!(["command"]));
    assert_eq!(bash["parameters"]["additionalProperties"], false);
    assert_eq!(bash["parameters"]["x-notSandboxed"], true);
    assert_eq!(
        bash["parameters"]["x-stdoutMaxBytes"],
        BASH_STDOUT_MAX_BYTES
    );
    let bash_description = bash["description"].as_str().unwrap();
    assert!(bash_description.contains("**Tool Name**: `bash`"));
    assert!(bash_description.contains("Run non-interactive commands from cwd"));
    assert!(bash_description.contains("instead of shell-based editing"));
}

#[test]
fn bash_rejects_obvious_cwd_escape_constructs_before_execution() {
    let (temp, runtime) = runtime();
    let outside = temp
        .path()
        .parent()
        .unwrap()
        .join("magi-code-outside-marker");
    let _ = fs::remove_file(&outside);

    for command in [
        "pwd > ../magi-code-outside-marker",
        "cat ../magi-code-outside-marker",
        "type ..\\magi-code-outside-marker",
        "type foo\\..\\magi-code-outside-marker",
        "cd .. && pwd",
        "type '..\\magi-code-outside-marker'",
        "type \"..\\magi-code-outside-marker\"",
    ] {
        let result = runtime.dispatch("bash", json!({"command": command}));
        assert!(!result.success, "accepted unsafe command: {command}");
        assert!(
            result.content.contains("cwd-scope preflight"),
            "unexpected error for {command}: {}",
            result.content
        );
    }
    assert!(!outside.exists());
}

#[test]
fn bash_absolute_paths_are_allowed_by_default_and_rejected_when_disabled() {
    let (_temp, runtime) = runtime();
    assert!(
        crate::shell::runtime::preflight_bash_cwd_scope(
            "cat /tmp/magi-code-marker",
            runtime.bash_absolute_paths,
            runtime.bash_shell_expansion,
        )
        .is_ok()
    );

    let restricted = ToolRuntime::new_with_settings(
        runtime.cwd.clone(),
        ToolSettings {
            bash: BashToolSettings {
                absolute_paths: false,
                ..BashToolSettings::default()
            },
            ..ToolSettings::default()
        },
    )
    .unwrap();
    let error = crate::shell::runtime::preflight_bash_cwd_scope(
        "cat /tmp/magi-code-marker",
        restricted.bash_absolute_paths,
        restricted.bash_shell_expansion,
    )
    .unwrap_err()
    .to_string();
    assert!(error.contains("tools.bash.absolute_paths"), "{error}");
}

#[test]
fn bash_shell_expansion_is_allowed_by_default_and_rejected_when_disabled() {
    let (_temp, runtime) = runtime();
    assert!(
        crate::shell::runtime::preflight_bash_cwd_scope(
            "echo $HOME",
            runtime.bash_absolute_paths,
            runtime.bash_shell_expansion,
        )
        .is_ok()
    );

    let restricted = ToolRuntime::new_with_settings(
        runtime.cwd.clone(),
        ToolSettings {
            bash: BashToolSettings {
                shell_expansion: false,
                ..BashToolSettings::default()
            },
            ..ToolSettings::default()
        },
    )
    .unwrap();
    let error = crate::shell::runtime::preflight_bash_cwd_scope(
        "echo $HOME",
        restricted.bash_absolute_paths,
        restricted.bash_shell_expansion,
    )
    .unwrap_err()
    .to_string();
    assert!(error.contains("tools.bash.shell_expansion"), "{error}");
}

#[cfg(unix)]
#[test]
fn bash_timeout_terminates_process_tree() {
    let (temp, runtime) = runtime();
    let marker = temp.path().join("late-marker");
    let result = runtime.dispatch(
        "bash",
        json!({"command":"(sleep 2; printf late > late-marker) & sleep 5", "timeout":1}),
    );
    assert!(!result.success);
    assert_eq!(result.metadata["timed_out"], true);
    thread::sleep(Duration::from_millis(1500));
    assert!(
        !marker.exists(),
        "descendant survived timeout and wrote marker"
    );
}

#[test]
fn bash_allows_safe_relative_commands() {
    let (temp, runtime) = runtime();
    let result = runtime.dispatch(
        "bash",
        json!({"command":"mkdir -p dir && printf ok > dir/file.txt && cat dir/file.txt"}),
    );
    assert!(result.success, "{}", result.content);
    assert_eq!(
        fs::read_to_string(temp.path().join("dir/file.txt")).unwrap(),
        "ok"
    );
    assert!(result.metadata["stdout"].as_str().unwrap().contains("ok"));

    let cd_current = runtime.dispatch("bash", json!({"command":"cd . && pwd"}));
    assert!(cd_current.success, "{}", cd_current.content);
}