use codex_features::Feature;use codex_features::Features;use codex_protocol::config_types::ModeKind;use codex_protocol::openai_models::ConfigShellToolType;use codex_protocol::openai_models::ModelInfo;use codex_protocol::openai_models::ModelVisibility;use codex_protocol::openai_models::TruncationPolicyConfig;use pretty_assertions::assert_eq;use super::*;fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo { ModelInfo { slug: "test-model".to_string(), display_name: "Test Model".to_string(), description: None, default_reasoning_level: None, supported_reasoning_levels: Vec::new(), shell_type, visibility: ModelVisibility::List, supported_in_api: true, priority: 0, additional_speed_tiers: Vec::new(), service_tiers: Vec::new(), default_service_tier: None, availability_nux: None, upgrade: None, base_instructions: String::new(), model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: Default::default(), support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, web_search_tool_type: Default::default(), truncation_policy: TruncationPolicyConfig::tokens(/*limit*/ 1024), supports_parallel_tool_calls: true, supports_image_detail_original: false, context_window: None, max_context_window: None, auto_compact_token_limit: None, comp_hash: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), input_modalities: codex_protocol::openai_models::default_input_modalities(), used_fallback_model_metadata: false, supports_search_tool: false, use_responses_lite: false, auto_review_model_override: None, tool_mode: None, multi_agent_version: None, }}fn shell_features() -> Features { let mut features = Features::with_defaults(); features.enable(Feature::ShellTool); features.disable(Feature::ShellZshFork); features.disable(Feature::UnifiedExec); features.disable(Feature::UnifiedExecZshFork); features}#[test]fn shell_type_is_derived_from_model_and_feature_gates() { let model = model_with_shell_type(ConfigShellToolType::UnifiedExec); let mut features = shell_features(); assert_eq!( shell_type_for_model_and_features(&model, &features), ConfigShellToolType::ShellCommand ); features.enable(Feature::UnifiedExec); let expected_unified_exec = if codex_utils_pty::conpty_supported() { ConfigShellToolType::UnifiedExec } else { ConfigShellToolType::ShellCommand }; assert_eq!( shell_type_for_model_and_features(&model, &features), expected_unified_exec ); features.enable(Feature::ShellZshFork); assert_eq!( shell_type_for_model_and_features(&model, &features), ConfigShellToolType::ShellCommand ); features.enable(Feature::UnifiedExecZshFork); assert_eq!( shell_type_for_model_and_features(&model, &features), expected_unified_exec ); features.disable(Feature::ShellTool); assert_eq!( shell_type_for_model_and_features(&model, &features), ConfigShellToolType::Disabled );}#[test]fn shell_command_backend_requires_both_shell_tool_and_zsh_fork() { let mut features = shell_features(); assert_eq!( shell_command_backend_for_features(&features), ShellCommandBackendConfig::Classic ); features.enable(Feature::ShellZshFork); assert_eq!( shell_command_backend_for_features(&features), ShellCommandBackendConfig::ZshFork ); features.disable(Feature::ShellTool); assert_eq!( shell_command_backend_for_features(&features), ShellCommandBackendConfig::Classic );}#[test]fn unified_exec_feature_mode_follows_composition_dependencies() { let mut features = shell_features(); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::Disabled ); features.enable(Feature::UnifiedExec); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::Direct ); features.enable(Feature::UnifiedExecZshFork); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::Direct ); features.enable(Feature::ShellZshFork); features.disable(Feature::UnifiedExecZshFork); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::Disabled ); features.enable(Feature::UnifiedExecZshFork); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::ZshFork ); features.disable(Feature::ShellTool); assert_eq!( unified_exec_feature_mode_for_features(&features), UnifiedExecFeatureMode::Disabled );}#[test]fn request_user_input_modes_follow_default_mode_feature() { let mut features = Features::with_defaults(); features.disable(Feature::DefaultModeRequestUserInput); assert_eq!( request_user_input_available_modes(&features), vec![ModeKind::Plan] ); features.enable(Feature::DefaultModeRequestUserInput); assert_eq!( request_user_input_available_modes(&features), vec![ModeKind::Default, ModeKind::Plan] );}#[test]fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() { let exe = std::env::current_exe().expect("current exe path"); let shell = exe.clone(); let mode = UnifiedExecShellMode::for_session( UnifiedExecFeatureMode::ZshFork, ToolUserShellType::Zsh, Some(&shell), Some(&exe), ); if cfg!(unix) { assert!(matches!(mode, UnifiedExecShellMode::ZshFork(_))); } else { assert_eq!(mode, UnifiedExecShellMode::Direct); } assert_eq!( UnifiedExecShellMode::for_session( UnifiedExecFeatureMode::Direct, ToolUserShellType::Zsh, Some(&shell), Some(&exe), ), UnifiedExecShellMode::Direct );}