Codex Handbook
core/src/tools/handlers/shell_tests.rs 250 lines
use std::path::PathBuf;use std::sync::Arc;use codex_protocol::models::ShellCommandToolCallParams;use pretty_assertions::assert_eq;use crate::exec_env::create_env;use crate::sandboxing::SandboxPermissions;use crate::session::tests::make_session_and_context;use crate::shell::Shell;use crate::shell::ShellType;use crate::tools::context::FunctionToolOutput;use crate::tools::context::ToolCallSource;use crate::tools::context::ToolInvocation;use crate::tools::context::ToolPayload;use crate::tools::handlers::ShellCommandHandler;use crate::tools::hook_names::HookToolName;use crate::tools::registry::CoreToolRuntime;use crate::turn_diff_tracker::TurnDiffTracker;use codex_shell_command::is_safe_command::is_known_safe_command;use codex_shell_command::powershell::try_find_powershell_executable_blocking;use codex_shell_command::powershell::try_find_pwsh_executable_blocking;use serde_json::json;use tokio::sync::Mutex;/// The logic for is_known_safe_command() has heuristics for known shells,/// so we must ensure the commands generated by [ShellCommandHandler] can be/// recognized as safe if the `command` is safe.#[test]fn commands_generated_by_shell_command_handler_can_be_matched_by_is_known_safe_command() {    let bash_shell = Shell {        shell_type: ShellType::Bash,        shell_path: PathBuf::from("/bin/bash"),    };    assert_safe(&bash_shell, "ls -la");    let zsh_shell = Shell {        shell_type: ShellType::Zsh,        shell_path: PathBuf::from("/bin/zsh"),    };    assert_safe(&zsh_shell, "ls -la");    if let Some(path) = try_find_powershell_executable_blocking() {        let powershell = Shell {            shell_type: ShellType::PowerShell,            shell_path: path.to_path_buf(),        };        assert_safe(&powershell, "ls -Name");    }    if let Some(path) = try_find_pwsh_executable_blocking() {        let pwsh = Shell {            shell_type: ShellType::PowerShell,            shell_path: path.to_path_buf(),        };        assert_safe(&pwsh, "ls -Name");    }}fn assert_safe(shell: &Shell, command: &str) {    assert!(is_known_safe_command(&shell.derive_exec_args(        command, /* use_login_shell */ /*use_login_shell*/ true    )));    assert!(is_known_safe_command(&shell.derive_exec_args(        command, /* use_login_shell */ /*use_login_shell*/ false    )));}#[tokio::test]async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() {    let (session, turn_context) = make_session_and_context().await;    let command = "echo hello".to_string();    let workdir = Some("subdir".to_string());    let login = None;    let timeout_ms = Some(1234);    let sandbox_permissions = SandboxPermissions::RequireEscalated;    let justification = Some("because tests".to_string());    let expected_command = session        .user_shell()        .derive_exec_args(&command, /*use_login_shell*/ true);    #[allow(deprecated)]    let expected_cwd = turn_context.resolve_path(workdir.clone());    let expected_env = create_env(        &turn_context.shell_environment_policy,        Some(session.thread_id),    );    let params = ShellCommandToolCallParams {        command,        workdir,        login,        timeout_ms,        sandbox_permissions: Some(sandbox_permissions),        additional_permissions: None,        prefix_rule: None,        justification: justification.clone(),    };    let exec_params = ShellCommandHandler::to_exec_params(        &params,        &session,        &turn_context,        session.thread_id,        /*allow_login_shell*/ true,    )    .expect("login shells should be allowed");    // ExecParams cannot derive Eq due to the CancellationToken field, so we manually compare the fields.    assert_eq!(exec_params.command, expected_command);    assert_eq!(exec_params.cwd, expected_cwd);    assert_eq!(exec_params.env, expected_env);    assert_eq!(exec_params.network, turn_context.network);    assert_eq!(exec_params.expiration.timeout_ms(), timeout_ms);    assert_eq!(exec_params.sandbox_permissions, sandbox_permissions);    assert_eq!(exec_params.justification, justification);    assert_eq!(exec_params.arg0, None);}#[test]fn shell_command_handler_respects_explicit_login_flag() {    let shell = Shell {        shell_type: ShellType::Bash,        shell_path: PathBuf::from("/bin/bash"),    };    let login_command = ShellCommandHandler::base_command(        &shell,        "echo login shell",        /*use_login_shell*/ true,    );    assert_eq!(        login_command,        shell.derive_exec_args("echo login shell", /*use_login_shell*/ true)    );    let non_login_command = ShellCommandHandler::base_command(        &shell,        "echo non login shell",        /*use_login_shell*/ false,    );    assert_eq!(        non_login_command,        shell.derive_exec_args("echo non login shell", /*use_login_shell*/ false)    );}#[tokio::test]async fn shell_command_handler_defaults_to_non_login_when_disallowed() {    let (session, turn_context) = make_session_and_context().await;    let params = ShellCommandToolCallParams {        command: "echo hello".to_string(),        workdir: None,        login: None,        timeout_ms: None,        sandbox_permissions: None,        additional_permissions: None,        prefix_rule: None,        justification: None,    };    let exec_params = ShellCommandHandler::to_exec_params(        &params,        &session,        &turn_context,        session.thread_id,        /*allow_login_shell*/ false,    )    .expect("non-login shells should still be allowed");    assert_eq!(        exec_params.command,        session            .user_shell()            .derive_exec_args("echo hello", /*use_login_shell*/ false)    );}#[test]fn shell_command_handler_rejects_login_when_disallowed() {    let err =        ShellCommandHandler::resolve_use_login_shell(Some(true), /*allow_login_shell*/ false)            .expect_err("explicit login should be rejected");    assert!(        err.to_string()            .contains("login shell is disabled by config"),        "unexpected error: {err}"    );}#[tokio::test]async fn shell_command_pre_tool_use_payload_uses_raw_command() {    let payload = ToolPayload::Function {        arguments: json!({ "command": "printf shell command" }).to_string(),    };    let (session, turn) = make_session_and_context().await;    let handler = ShellCommandHandler::from(codex_tools::ShellCommandBackendConfig::Classic);    assert_eq!(        handler.pre_tool_use_payload(&ToolInvocation {            session: session.into(),            turn: turn.into(),            cancellation_token: tokio_util::sync::CancellationToken::new(),            tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),            call_id: "call-42".to_string(),            tool_name: codex_tools::ToolName::plain("shell_command"),            source: crate::tools::context::ToolCallSource::Direct,            payload,        }),        Some(crate::tools::registry::PreToolUsePayload {            tool_name: HookToolName::bash(),            tool_input: json!({ "command": "printf shell command" }),        })    );}#[tokio::test]async fn build_post_tool_use_payload_uses_tool_output_wire_value() {    let payload = ToolPayload::Function {        arguments: json!({ "command": "printf shell command" }).to_string(),    };    let output = FunctionToolOutput {        body: vec![],        success: Some(true),        post_tool_use_response: Some(json!("shell output")),    };    let handler = ShellCommandHandler::from(codex_tools::ShellCommandBackendConfig::Classic);    let (session, turn) = make_session_and_context().await;    let invocation = ToolInvocation {        session: session.into(),        turn: turn.into(),        cancellation_token: tokio_util::sync::CancellationToken::new(),        tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),        call_id: "call-42".to_string(),        tool_name: codex_tools::ToolName::plain("shell_command"),        source: ToolCallSource::Direct,        payload,    };    assert_eq!(        handler.post_tool_use_payload(&invocation, &output),        Some(crate::tools::registry::PostToolUsePayload {            tool_name: HookToolName::bash(),            tool_use_id: "call-42".to_string(),            tool_input: json!({ "command": "printf shell command" }),            tool_response: json!("shell output"),        })    );}