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( ¶ms, &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( ¶ms, &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"), }) );}