aboutsummaryrefslogtreecommitdiff
path: root/tests/cli_wrapper.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 13:29:57 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-31 13:29:57 +0300
commitf8c452294d6f76ccb264c0d9e205398492f00e18 (patch)
tree68f9505424de441b5da4dbe26fab6149e2a75734 /tests/cli_wrapper.rs
parentb8ccdf600c750411b439d0eb41f6a70e3c595f4f (diff)
fix: terminate pane sessions on close
Diffstat (limited to 'tests/cli_wrapper.rs')
-rw-r--r--tests/cli_wrapper.rs142
1 files changed, 142 insertions, 0 deletions
diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs
index c8dbc8b8..dd52c082 100644
--- a/tests/cli_wrapper.rs
+++ b/tests/cli_wrapper.rs
@@ -71,6 +71,26 @@ fn run_cli(socket_path: &Path, args: &[&str]) -> std::process::Output {
command.output().unwrap()
}
+fn process_exists(pid: u32) -> bool {
+ let result = unsafe { libc::kill(pid as i32, 0) };
+ if result == 0 {
+ true
+ } else {
+ std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
+ }
+}
+
+fn wait_for_pid_exit(pid: u32, timeout: Duration) -> bool {
+ let deadline = Instant::now() + timeout;
+ while Instant::now() < deadline {
+ if !process_exists(pid) {
+ return true;
+ }
+ thread::sleep(Duration::from_millis(25));
+ }
+ !process_exists(pid)
+}
+
fn send_request(socket_path: &Path, json: &str) -> serde_json::Value {
let mut stream = UnixStream::connect(socket_path).unwrap();
stream.write_all(json.as_bytes()).unwrap();
@@ -228,6 +248,128 @@ fn pane_run_read_and_wait_commands_work() {
}
#[test]
+fn closing_pane_terminates_processes_inside_it() {
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let socket_path = runtime_dir.join("herdr.sock");
+
+ let mut herdr = spawn_herdr(&config_home, &runtime_dir, &socket_path);
+ wait_for_socket(&socket_path, Duration::from_secs(5));
+
+ let created = run_cli(
+ &socket_path,
+ &["workspace", "create", "--cwd", base.to_str().unwrap()],
+ );
+ assert!(created.status.success());
+
+ let split = run_cli(
+ &socket_path,
+ &["pane", "split", "1-1", "--direction", "right"],
+ );
+ assert!(split.status.success());
+ let split_json: serde_json::Value = serde_json::from_slice(&split.stdout).unwrap();
+ let pane_id = split_json["result"]["pane"]["pane_id"].as_str().unwrap();
+
+ let pid_file = base.join("pane-close.pid");
+ let command = format!(
+ "python3 -c 'import os,time,pathlib; pathlib.Path(r\"{}\").write_text(str(os.getpid())); time.sleep(1000)'",
+ pid_file.display()
+ );
+ let ran = run_cli(&socket_path, &["pane", "run", pane_id, &command]);
+ assert!(
+ ran.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&ran.stderr)
+ );
+
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while Instant::now() < deadline && !pid_file.exists() {
+ thread::sleep(Duration::from_millis(25));
+ }
+ assert!(pid_file.exists(), "pid file was not created");
+
+ let pid: u32 = fs::read_to_string(&pid_file)
+ .unwrap()
+ .trim()
+ .parse()
+ .unwrap();
+ assert!(process_exists(pid), "child process was not running");
+
+ let closed = run_cli(&socket_path, &["pane", "close", pane_id]);
+ assert!(
+ closed.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&closed.stderr)
+ );
+ assert!(
+ wait_for_pid_exit(pid, Duration::from_secs(3)),
+ "process {pid} survived pane close"
+ );
+
+ let _ = herdr.child.kill();
+ let _ = herdr.child.wait();
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn closing_workspace_terminates_processes_inside_it() {
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let socket_path = runtime_dir.join("herdr.sock");
+
+ let mut herdr = spawn_herdr(&config_home, &runtime_dir, &socket_path);
+ wait_for_socket(&socket_path, Duration::from_secs(5));
+
+ let created = run_cli(
+ &socket_path,
+ &["workspace", "create", "--cwd", base.to_str().unwrap()],
+ );
+ assert!(created.status.success());
+
+ let pid_file = base.join("workspace-close.pid");
+ let command = format!(
+ "python3 -c 'import os,time,pathlib; pathlib.Path(r\"{}\").write_text(str(os.getpid())); time.sleep(1000)'",
+ pid_file.display()
+ );
+ let ran = run_cli(&socket_path, &["pane", "run", "1-1", &command]);
+ assert!(
+ ran.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&ran.stderr)
+ );
+
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while Instant::now() < deadline && !pid_file.exists() {
+ thread::sleep(Duration::from_millis(25));
+ }
+ assert!(pid_file.exists(), "pid file was not created");
+
+ let pid: u32 = fs::read_to_string(&pid_file)
+ .unwrap()
+ .trim()
+ .parse()
+ .unwrap();
+ assert!(process_exists(pid), "child process was not running");
+
+ let closed = run_cli(&socket_path, &["workspace", "close", "1"]);
+ assert!(
+ closed.status.success(),
+ "stderr: {}",
+ String::from_utf8_lossy(&closed.stderr)
+ );
+ assert!(
+ wait_for_pid_exit(pid, Duration::from_secs(3)),
+ "process {pid} survived workspace close"
+ );
+
+ let _ = herdr.child.kill();
+ let _ = herdr.child.wait();
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
fn ids_are_compact_and_positional() {
let base = unique_test_dir();
let config_home = base.join("config");