Files
codex/codex-rs/core/src/shell_tests.rs
T
pakrym-oai 1aa3e432cc [codex] simplify shell snapshot ownership (#27756)
## Why

Shell snapshot lifecycle state was split between `Shell` and
`SessionServices`: `Shell` carried the receiver while session code
exposed and forwarded the raw sender. That coupled shell identity to
mutable snapshot state and made refresh, inheritance, and file lifetime
harder to reason about.

## What changed

- make each `Arc<ShellSnapshot>` represent one cwd-specific snapshot
generation
- store the active generation in `SessionServices` with `ArcSwapOption`
- have construction start the background build and expose only a
cwd-validated snapshot path
- use `ShellSnapshotFile` ownership to delete snapshot files
automatically
- pass snapshot paths explicitly to shell runtimes instead of storing
snapshot state on `Shell`
- preserve inherited and in-flight generations by pinning their `Arc`
while they are in use

## Test plan

- `cargo check -p codex-core --lib`
- `just test -p codex-core 'shell_snapshot::tests'`
- `just test -p codex-core
shell_command_snapshot_still_intercepts_apply_patch`
- `just test -p codex-core
shell_snapshot_deleted_after_shutdown_with_skills`
2026-06-15 08:18:13 -07:00

189 lines
5.1 KiB
Rust

use super::*;
use std::path::PathBuf;
use std::process::Command;
#[test]
#[cfg(target_os = "macos")]
fn detects_zsh() {
let zsh_shell = get_shell(ShellType::Zsh, /*path*/ None).unwrap();
let shell_path = zsh_shell.shell_path;
assert_eq!(shell_path, std::path::Path::new("/bin/zsh"));
}
#[test]
#[cfg(target_os = "macos")]
fn fish_fallback_to_zsh() {
let zsh_shell = default_user_shell_from_path(Some(PathBuf::from("/bin/fish")));
let shell_path = zsh_shell.shell_path;
assert_eq!(shell_path, std::path::Path::new("/bin/zsh"));
}
#[test]
fn detects_bash() {
let bash_shell = get_shell(ShellType::Bash, /*path*/ None).unwrap();
let shell_path = bash_shell.shell_path;
assert!(
shell_path.file_name().and_then(|name| name.to_str()) == Some("bash"),
"shell path: {shell_path:?}",
);
}
#[test]
fn detects_sh() {
let sh_shell = get_shell(ShellType::Sh, /*path*/ None).unwrap();
let shell_path = sh_shell.shell_path;
assert!(
shell_path.file_name().and_then(|name| name.to_str()) == Some("sh"),
"shell path: {shell_path:?}",
);
}
#[test]
fn can_run_on_shell_test() {
let cmd = "echo \"Works\"";
if cfg!(windows) {
assert!(shell_works(
get_shell(ShellType::PowerShell, /*path*/ None),
"Out-String 'Works'",
/*required*/ true,
));
assert!(shell_works(
get_shell(ShellType::Cmd, /*path*/ None),
cmd,
/*required*/ true,
));
assert!(shell_works(
Some(ultimate_fallback_shell()),
cmd,
/*required*/ true
));
} else {
assert!(shell_works(
Some(ultimate_fallback_shell()),
cmd,
/*required*/ true
));
assert!(shell_works(
get_shell(ShellType::Zsh, /*path*/ None),
cmd,
/*required*/ false
));
assert!(shell_works(
get_shell(ShellType::Bash, /*path*/ None),
cmd,
/*required*/ true
));
assert!(shell_works(
get_shell(ShellType::Sh, /*path*/ None),
cmd,
/*required*/ true
));
}
}
fn shell_works(shell: Option<Shell>, command: &str, required: bool) -> bool {
if let Some(shell) = shell {
let args = shell.derive_exec_args(command, /*use_login_shell*/ false);
let output = Command::new(args[0].clone())
.args(&args[1..])
.output()
.unwrap();
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("Works"));
true
} else {
!required
}
}
#[test]
fn derive_exec_args() {
let test_bash_shell = Shell {
shell_type: ShellType::Bash,
shell_path: PathBuf::from("/bin/bash"),
};
assert_eq!(
test_bash_shell.derive_exec_args("echo hello", /*use_login_shell*/ false),
vec!["/bin/bash", "-c", "echo hello"]
);
assert_eq!(
test_bash_shell.derive_exec_args("echo hello", /*use_login_shell*/ true),
vec!["/bin/bash", "-lc", "echo hello"]
);
let test_zsh_shell = Shell {
shell_type: ShellType::Zsh,
shell_path: PathBuf::from("/bin/zsh"),
};
assert_eq!(
test_zsh_shell.derive_exec_args("echo hello", /*use_login_shell*/ false),
vec!["/bin/zsh", "-c", "echo hello"]
);
assert_eq!(
test_zsh_shell.derive_exec_args("echo hello", /*use_login_shell*/ true),
vec!["/bin/zsh", "-lc", "echo hello"]
);
let test_powershell_shell = Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from("pwsh.exe"),
};
assert_eq!(
test_powershell_shell.derive_exec_args("echo hello", /*use_login_shell*/ false),
vec!["pwsh.exe", "-NoProfile", "-Command", "echo hello"]
);
assert_eq!(
test_powershell_shell.derive_exec_args("echo hello", /*use_login_shell*/ true),
vec!["pwsh.exe", "-Command", "echo hello"]
);
}
#[tokio::test]
async fn test_current_shell_detects_zsh() {
let shell = Command::new("sh")
.arg("-c")
.arg("echo $SHELL")
.output()
.unwrap();
let shell_path = String::from_utf8_lossy(&shell.stdout).trim().to_string();
if shell_path.ends_with("/zsh") {
assert_eq!(
default_user_shell(),
Shell {
shell_type: ShellType::Zsh,
shell_path: PathBuf::from(shell_path),
}
);
}
}
#[tokio::test]
async fn detects_powershell_as_default() {
if !cfg!(windows) {
return;
}
let powershell_shell = default_user_shell();
let shell_path = powershell_shell.shell_path;
assert!(shell_path.ends_with("pwsh.exe") || shell_path.ends_with("powershell.exe"));
}
#[test]
fn finds_powershell() {
if !cfg!(windows) {
return;
}
let powershell_shell = get_shell(ShellType::PowerShell, /*path*/ None).unwrap();
let shell_path = powershell_shell.shell_path;
assert!(shell_path.ends_with("pwsh.exe") || shell_path.ends_with("powershell.exe"));
}