Codex Handbook
core/src/context/environment_context_tests.rs 432 lines
use crate::shell::ShellType;use super::*;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemAccessMode;use codex_protocol::permissions::FileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::permissions::FileSystemSpecialPath;use codex_protocol::permissions::NetworkSandboxPolicy;use codex_protocol::permissions::project_roots_glob_pattern;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::SandboxPolicy;use codex_protocol::protocol::TurnContextItem;use codex_utils_absolute_path::test_support::PathBufExt;use core_test_support::test_path_buf;use pretty_assertions::assert_eq;use std::path::Path;use std::path::PathBuf;fn fake_shell_name() -> String {    let shell = crate::shell::Shell {        shell_type: ShellType::Bash,        shell_path: PathBuf::from("/bin/bash"),    };    shell.name().to_string()}fn test_abs_path(unix_path: &str) -> AbsolutePathBuf {    test_path_buf(unix_path).abs()}#[test]fn serialize_workspace_write_environment_context() {    let cwd = test_path_buf("/repo");    let context = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: cwd.abs(),            shell: fake_shell_name(),        }],        Some("2026-02-26".to_string()),        Some("America/Los_Angeles".to_string()),        /*network*/ None,        /*subagents*/ None,    );    let expected = format!(        r#"<environment_context>  <cwd>{cwd}</cwd>  <shell>bash</shell>  <current_date>2026-02-26</current_date>  <timezone>America/Los_Angeles</timezone></environment_context>"#,        cwd = cwd.display(),    );    assert_eq!(context.render(), expected);}#[test]fn serialize_environment_context_with_network() {    let network = NetworkContext::new(        vec!["api.example.com".to_string(), "*.openai.com".to_string()],        vec!["blocked.example.com".to_string()],    );    let context = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_path_buf("/repo").abs(),            shell: fake_shell_name(),        }],        Some("2026-02-26".to_string()),        Some("America/Los_Angeles".to_string()),        Some(network),        /*subagents*/ None,    );    let expected = format!(        r#"<environment_context>  <cwd>{}</cwd>  <shell>bash</shell>  <current_date>2026-02-26</current_date>  <timezone>America/Los_Angeles</timezone>  <network enabled="true"><allowed>api.example.com,*.openai.com</allowed><denied>blocked.example.com</denied></network></environment_context>"#,        test_path_buf("/repo").display()    );    assert_eq!(context.render(), expected);}fn workspace_write_permission_profile_with_private_denials() -> PermissionProfile {    PermissionProfile::from_runtime_permissions(        &FileSystemSandboxPolicy::restricted(vec![            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::project_roots(/*subpath*/ None),                },                access: FileSystemAccessMode::Write,            },            FileSystemSandboxEntry {                path: FileSystemPath::Special {                    value: FileSystemSpecialPath::project_roots(Some(PathBuf::from("private"))),                },                access: FileSystemAccessMode::Deny,            },            FileSystemSandboxEntry {                path: FileSystemPath::GlobPattern {                    pattern: project_roots_glob_pattern(Path::new("private/**")),                },                access: FileSystemAccessMode::Deny,            },        ]),        NetworkSandboxPolicy::Restricted,    )}#[test]fn serialize_environment_context_with_full_filesystem_profile() {    let repo = test_abs_path("/repo");    let other_repo = test_abs_path("/other-repo");    let repo_private = repo.join("private");    let other_repo_private = other_repo.join("private");    let repo_private_glob =        AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), repo.as_path());    let other_repo_private_glob =        AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), other_repo.as_path());    let mut context = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_path_buf("/repo").abs(),            shell: fake_shell_name(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    context.filesystem = Some(FileSystemContext::from_permission_profile(        &workspace_write_permission_profile_with_private_denials(),        &[repo.clone(), other_repo.clone()],    ));    let expected = format!(        r#"<environment_context>  <cwd>{}</cwd>  <shell>bash</shell>  <filesystem><workspace_roots><root>{repo}</root><root>{other_repo}</root></workspace_roots><permission_profile type="managed"><file_system type="restricted"><entry access="write"><path>{repo}</path></entry><entry access="write"><path>{other_repo}</path></entry><entry access="deny" escalatable="false"><path>{repo_private}</path></entry><entry access="deny" escalatable="false"><path>{other_repo_private}</path></entry><entry access="deny" escalatable="false"><glob>{repo_private_glob}</glob></entry><entry access="deny" escalatable="false"><glob>{other_repo_private_glob}</glob></entry></file_system></permission_profile></filesystem></environment_context>"#,        test_path_buf("/repo").display(),        repo = repo.to_string_lossy(),        other_repo = other_repo.to_string_lossy(),        repo_private = repo_private.to_string_lossy(),        other_repo_private = other_repo_private.to_string_lossy(),        repo_private_glob = repo_private_glob.to_string_lossy(),        other_repo_private_glob = other_repo_private_glob.to_string_lossy(),    );    assert_eq!(context.render(), expected);}#[test]fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {    let repo = test_abs_path("/repo");    let other_repo = test_abs_path("/other-repo");    let repo_private = repo.join("private");    let item = TurnContextItem {        turn_id: None,        cwd: test_path_buf("/not-the-workspace"),        workspace_roots: Some(vec![repo.clone(), other_repo.clone()]),        current_date: None,        timezone: None,        approval_policy: AskForApproval::Never,        sandbox_policy: SandboxPolicy::new_read_only_policy(),        permission_profile: Some(workspace_write_permission_profile_with_private_denials()),        network: None,        file_system_sandbox_policy: None,        model: "gpt-5".to_string(),        comp_hash: None,        personality: None,        collaboration_mode: None,        multi_agent_version: None,        realtime_active: None,        effort: None,        summary: codex_protocol::config_types::ReasoningSummary::Auto,    };    let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render();    assert!(        context.contains(&format!(            "<root>{}</root><root>{}</root>",            repo.to_string_lossy(),            other_repo.to_string_lossy()        )),        "{context}"    );    assert!(        context.contains(&format!("<path>{}</path>", repo_private.to_string_lossy())),        "{context}"    );    assert!(        !context.contains(            test_abs_path("/not-the-workspace")                .join("private")                .to_string_lossy()                .as_ref()        ),        "{context}"    );}#[test]fn serialize_read_only_environment_context() {    let context = EnvironmentContext::new(        Vec::new(),        Some("2026-02-26".to_string()),        Some("America/Los_Angeles".to_string()),        /*network*/ None,        /*subagents*/ None,    );    let expected = r#"<environment_context>  <current_date>2026-02-26</current_date>  <timezone>America/Los_Angeles</timezone></environment_context>"#;    assert_eq!(context.render(), expected);}#[test]fn equals_except_shell_compares_cwd() {    let context1 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_abs_path("/repo"),            shell: fake_shell_name(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    let context2 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_abs_path("/repo"),            shell: fake_shell_name(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    assert!(context1.equals_except_shell(&context2));}#[test]fn equals_except_shell_compares_cwd_differences() {    let context1 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_abs_path("/repo1"),            shell: fake_shell_name(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    let context2 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_abs_path("/repo2"),            shell: fake_shell_name(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    assert!(!context1.equals_except_shell(&context2));}#[test]fn equals_except_shell_ignores_shell() {    let context1 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_abs_path("/repo"),            shell: "bash".to_string(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    let context2 = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "other".to_string(),            cwd: test_abs_path("/repo"),            shell: "zsh".to_string(),        }],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    assert!(context1.equals_except_shell(&context2));}#[test]fn serialize_environment_context_with_subagents() {    let context = EnvironmentContext::new(        vec![EnvironmentContextEnvironment {            id: "local".to_string(),            cwd: test_path_buf("/repo").abs(),            shell: fake_shell_name(),        }],        Some("2026-02-26".to_string()),        Some("America/Los_Angeles".to_string()),        /*network*/ None,        Some("- agent-1: atlas\n- agent-2".to_string()),    );    let expected = format!(        r#"<environment_context>  <cwd>{}</cwd>  <shell>bash</shell>  <current_date>2026-02-26</current_date>  <timezone>America/Los_Angeles</timezone>  <subagents>    - agent-1: atlas    - agent-2  </subagents></environment_context>"#,        test_path_buf("/repo").display()    );    assert_eq!(context.render(), expected);}#[test]fn serialize_environment_context_with_multiple_selected_environments() {    let local_cwd = test_path_buf("/repo/local");    let remote_cwd = test_path_buf("/repo/remote");    let context = EnvironmentContext::new(        vec![            EnvironmentContextEnvironment {                id: "local".to_string(),                cwd: local_cwd.abs(),                shell: "bash".to_string(),            },            EnvironmentContextEnvironment {                id: "remote".to_string(),                cwd: remote_cwd.abs(),                shell: "bash".to_string(),            },        ],        Some("2026-02-26".to_string()),        Some("America/Los_Angeles".to_string()),        /*network*/ None,        /*subagents*/ None,    );    let expected = format!(        r#"<environment_context>  <environments>    <environment id="local">      <cwd>{}</cwd>      <shell>bash</shell>    </environment>    <environment id="remote">      <cwd>{}</cwd>      <shell>bash</shell>    </environment>  </environments>  <current_date>2026-02-26</current_date>  <timezone>America/Los_Angeles</timezone></environment_context>"#,        local_cwd.display(),        remote_cwd.display()    );    assert_eq!(context.render(), expected);}#[test]fn serialize_environment_context_prefers_environment_shell_when_present() {    let local_cwd = test_path_buf("/repo/local");    let remote_cwd = test_path_buf("/repo/remote");    let context = EnvironmentContext::new(        vec![            EnvironmentContextEnvironment {                id: "local".to_string(),                cwd: local_cwd.abs(),                shell: "powershell".to_string(),            },            EnvironmentContextEnvironment {                id: "remote".to_string(),                cwd: remote_cwd.abs(),                shell: "cmd".to_string(),            },        ],        /*current_date*/ None,        /*timezone*/ None,        /*network*/ None,        /*subagents*/ None,    );    let expected = format!(        r#"<environment_context>  <environments>    <environment id="local">      <cwd>{}</cwd>      <shell>powershell</shell>    </environment>    <environment id="remote">      <cwd>{}</cwd>      <shell>cmd</shell>    </environment>  </environments></environment_context>"#,        local_cwd.display(),        remote_cwd.display()    );    assert_eq!(context.render(), expected);}