use std::path::{Path, PathBuf};
use crate::tui::editor::{
EDITOR_ALLOWLIST, WaitOutcome, decode_wait_status, handoff_target, resolve_editor, split_argv,
};
#[test]
fn editor_allowlist_is_the_four_named_editors() {
assert_eq!(EDITOR_ALLOWLIST, &["vi", "vim", "nano", "emacs"]);
}
#[test]
fn handoff_target_matches_bare_editor_names() {
assert_eq!(handoff_target("vi"), Some("vi"));
assert_eq!(handoff_target("vim"), Some("vim"));
assert_eq!(handoff_target("nano"), Some("nano"));
assert_eq!(handoff_target("emacs"), Some("emacs"));
}
#[test]
fn handoff_target_matches_editors_with_arguments() {
assert_eq!(handoff_target("vi notes.md"), Some("vi"));
assert_eq!(handoff_target("vim src/main.rs +10"), Some("vim"));
assert_eq!(handoff_target("nano TODO"), Some("nano"));
assert_eq!(handoff_target("emacs -nw file.txt"), Some("emacs"));
}
#[test]
fn handoff_target_rejects_lookalikes_and_non_editors() {
assert_eq!(handoff_target("myvi-helper"), None);
assert_eq!(handoff_target("vim-tiny notes.md"), None);
assert_eq!(handoff_target("echo vi"), None);
assert_eq!(handoff_target("ls"), None);
assert_eq!(handoff_target("/usr/bin/vi notes.md"), None);
assert_eq!(handoff_target("./vi"), None);
assert_eq!(handoff_target(""), None);
assert_eq!(handoff_target(" "), None);
}
#[test]
fn split_argv_program_and_args() {
assert_eq!(
split_argv("vi notes.md"),
Some(("vi".to_string(), vec!["notes.md".to_string()]))
);
assert_eq!(split_argv("nano"), Some(("nano".to_string(), vec![])));
assert_eq!(
split_argv(" vi a b "),
Some(("vi".to_string(), vec!["a".into(), "b".into()]))
);
assert_eq!(split_argv(""), None);
assert_eq!(split_argv(" "), None);
}
fn fake_path_with(dir: &Path, names: &[&str]) -> Vec<PathBuf> {
names
.iter()
.map(|name| {
let p = dir.join(name);
std::fs::write(&p, "#!/bin/sh\nexit 0\n").expect("write fake binary");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755))
.expect("chmod fake binary");
}
p
})
.collect()
}
#[test]
fn resolve_editor_finds_executables_on_the_given_path() {
let dir = tempfile::tempdir().unwrap();
fake_path_with(dir.path(), &["vi", "nano"]);
let path_dirs = dir.path().to_string_lossy().to_string();
assert_eq!(
resolve_editor("vi", Some(&path_dirs), dir.path()),
Some(dir.path().join("vi"))
);
assert_eq!(
resolve_editor("nano", Some(&path_dirs), dir.path()),
Some(dir.path().join("nano"))
);
assert_eq!(resolve_editor("emacs", Some(&path_dirs), dir.path()), None);
assert_eq!(
resolve_editor("nonexistent-editor-xyz", Some(&path_dirs), dir.path()),
None
);
}
#[test]
fn resolve_editor_ignores_non_executable_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("vi"), "not executable").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
dir.path().join("vi"),
std::fs::Permissions::from_mode(0o644),
)
.unwrap();
let path_dirs = dir.path().to_string_lossy().to_string();
assert_eq!(resolve_editor("vi", Some(&path_dirs), dir.path()), None);
}
}
#[test]
fn resolve_editor_rejects_path_dir_that_does_not_exist() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
resolve_editor("vi", Some("/nonexistent-dir-for-1744"), dir.path()),
None
);
}
#[test]
fn wait_status_decode_covers_all_three_posix_cases() {
assert_eq!(decode_wait_status(0x0000), WaitOutcome::Exited(0));
assert_eq!(decode_wait_status(0x0100), WaitOutcome::Exited(1));
assert_eq!(decode_wait_status(0x147F), WaitOutcome::Stopped(20)); assert_eq!(decode_wait_status(0x0009), WaitOutcome::Signaled(9)); assert_eq!(decode_wait_status(0x000F), WaitOutcome::Signaled(15)); }
#[test]
fn non_editor_bangs_stay_on_the_pipe_path() {
assert!(handoff_target("ls -la").is_none());
assert!(handoff_target("git status").is_none());
assert!(handoff_target("echo vi > /dev/null").is_none());
}
#[test]
fn bang_branch_routes_allowlist_into_the_parking_field() {
let src = include_str!("../tui/app/input.rs");
assert!(
src.contains("if crate::tui::editor::handoff_target(&shell_cmd).is_some()")
&& src.contains("self.pending_editor_handoff = Some((shell_cmd, origin_session));"),
"bang branch must park handoff_target matches as (cmd, origin_session) for the runner loop"
);
let runner = include_str!("../tui/runner.rs");
assert!(
runner.contains("app.pending_editor_handoff.take()")
&& runner.contains("mouse_capture_applied = true;"),
"runner loop must consume the parked handoff and resync mouse capture after it"
);
}
#[tokio::test]
async fn a_stopped_child_is_coerced_to_exit_instead_of_hanging_the_reap() {
use std::process::Stdio;
let child = std::process::Command::new("/bin/sh")
.arg("-c")
.arg("kill -STOP $$; sleep 30")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let pid = child.id() as libc::pid_t;
std::mem::forget(child);
let done = tokio::time::timeout(
std::time::Duration::from_secs(5),
crate::tui::editor::reap_editor(pid),
)
.await;
assert!(
done.is_ok(),
"reap_editor hung on a stopped child (#1744 regression)"
);
}
#[test]
fn editor_module_is_unix_gated_at_the_mod_declaration() {
let decl = include_str!("../tui/mod.rs");
assert!(
decl.contains("#[cfg(unix)]\npub mod editor;"),
"the editor module must stay cfg(unix): its reap path is libc-based (#1755)"
);
}