aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-07 16:14:46 +0300
committerGitHub <noreply@github.com>2026-09-07 15:14:46 +0200
commite7d8220788b4fa7c266a1e4457591b694478771c (patch)
tree272303d74e0b58c11f9c0c434dae8335d3d06703 /tests
parent482818130407691154d1635d4029b95d0e730104 (diff)
fix: preserve sessions after interrupted pane exits (#3418)
* fix: preserve sessions when pane shells are signaled refs #3415 * fix: preserve shutdown checkpoints across platforms refs #3415 --------- Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/api_ping.rs72
-rw-r--r--tests/live_handoff.rs90
2 files changed, 162 insertions, 0 deletions
diff --git a/tests/api_ping.rs b/tests/api_ping.rs
index fcfaf41f..b86d0378 100644
--- a/tests/api_ping.rs
+++ b/tests/api_ping.rs
@@ -356,6 +356,78 @@ contains = ["server-reload-marker"]
cleanup_spawned_herdr(child, base);
}
+#[cfg(target_os = "linux")]
+#[test]
+fn shutdown_preserves_session_after_shell_is_signaled() {
+ let _lock = test_lock();
+ 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 child = spawn_herdr_with_shell(&config_home, &runtime_dir, &socket_path, "/bin/sh");
+ wait_for_socket(&socket_path, Duration::from_secs(5));
+
+ let created = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"create","method":"workspace.create","params":{{"cwd":"{}","focus":true}}}}"#,
+ base.display()
+ ),
+ );
+ let pane_id = created["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("root pane id");
+ let process_info = send_request(
+ &socket_path,
+ &format!(
+ r#"{{"id":"process","method":"pane.process_info","params":{{"pane_id":"{pane_id}"}}}}"#
+ ),
+ );
+ let shell_pid = process_info["result"]["process_info"]["shell_pid"]
+ .as_u64()
+ .expect("shell pid") as libc::pid_t;
+
+ assert_eq!(unsafe { libc::kill(shell_pid, libc::SIGHUP) }, 0);
+
+ let deadline = Instant::now() + Duration::from_secs(2);
+ loop {
+ let panes = send_request(
+ &socket_path,
+ r#"{"id":"panes","method":"pane.list","params":{}}"#,
+ );
+ if panes["result"]["panes"]
+ .as_array()
+ .is_some_and(Vec::is_empty)
+ {
+ break;
+ }
+ assert!(Instant::now() < deadline, "signaled pane was not removed");
+ thread::sleep(Duration::from_millis(20));
+ }
+
+ let stopped = send_request(
+ &socket_path,
+ r#"{"id":"stop","method":"server.stop","params":{}}"#,
+ );
+ assert_eq!(stopped["result"]["type"], "ok");
+ child.child.wait().expect("server should stop cleanly");
+
+ let session: serde_json::Value = serde_json::from_slice(
+ &fs::read(config_home.join("herdr-dev/session.json")).expect("saved session"),
+ )
+ .expect("valid session json");
+ assert_eq!(session["workspaces"].as_array().map(Vec::len), Some(1));
+ assert_eq!(
+ session["workspaces"][0]["tabs"][0]["panes"]
+ .as_object()
+ .map(serde_json::Map::len),
+ Some(1)
+ );
+
+ cleanup_spawned_herdr(child, base);
+}
+
#[cfg(not(target_os = "macos"))]
#[test]
fn workspace_list_and_create_round_trip() {
diff --git a/tests/live_handoff.rs b/tests/live_handoff.rs
index 3838b888..034e1e89 100644
--- a/tests/live_handoff.rs
+++ b/tests/live_handoff.rs
@@ -612,6 +612,96 @@ fn live_server_holds_one_pty_master_fd_per_pane() {
cleanup_test_base(&base);
}
+#[cfg(target_os = "linux")]
+#[test]
+fn live_handoff_unknown_pane_exit_preserves_session_on_shutdown() {
+ let _lock = test_lock();
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let api_socket = runtime_dir.join("herdr.sock");
+
+ let spawned = spawn_server(&config_home, &runtime_dir, &api_socket);
+ wait_for_socket(&api_socket, Duration::from_secs(10));
+ register_runtime_dir(&runtime_dir);
+
+ let created = request(
+ &api_socket,
+ serde_json::json!({
+ "id": "test:workspace:create",
+ "method": "workspace.create",
+ "params": {"cwd": "/tmp", "focus": true}
+ }),
+ );
+ let pane_id = created["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("root pane id")
+ .to_string();
+ let old_pid = spawned.child.process_id().expect("old server pid");
+
+ assert_ok(request(
+ &api_socket,
+ serde_json::json!({"id":"test:handoff","method":"server.live_handoff","params":{}}),
+ ));
+ let replacement_pid =
+ wait_for_replacement_server_pid(&runtime_dir, old_pid, Duration::from_secs(10));
+ drop(spawned);
+ wait_for_api(&api_socket, Duration::from_secs(10));
+
+ let process_info = request(
+ &api_socket,
+ serde_json::json!({
+ "id": "test:process-info",
+ "method": "pane.process_info",
+ "params": {"pane_id": pane_id}
+ }),
+ );
+ let shell_pid = process_info["result"]["process_info"]["shell_pid"]
+ .as_u64()
+ .expect("shell pid") as libc::pid_t;
+ assert_eq!(unsafe { libc::kill(shell_pid, libc::SIGHUP) }, 0);
+
+ let deadline = Instant::now() + Duration::from_secs(2);
+ loop {
+ let panes = request(
+ &api_socket,
+ serde_json::json!({"id":"test:panes","method":"pane.list","params":{}}),
+ );
+ if panes["result"]["panes"]
+ .as_array()
+ .is_some_and(Vec::is_empty)
+ {
+ break;
+ }
+ assert!(Instant::now() < deadline, "handoff pane was not removed");
+ thread::sleep(Duration::from_millis(20));
+ }
+
+ assert_ok(request(
+ &api_socket,
+ serde_json::json!({"id":"test:stop","method":"server.stop","params":{}}),
+ ));
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while Path::new(&format!("/proc/{replacement_pid}")).exists() {
+ assert!(Instant::now() < deadline, "replacement server did not stop");
+ thread::sleep(Duration::from_millis(20));
+ }
+
+ let session: serde_json::Value = serde_json::from_slice(
+ &fs::read(config_home.join("herdr-dev/session.json")).expect("saved session"),
+ )
+ .expect("valid session json");
+ assert_eq!(session["workspaces"].as_array().map(Vec::len), Some(1));
+ assert_eq!(
+ session["workspaces"][0]["tabs"][0]["panes"]
+ .as_object()
+ .map(serde_json::Map::len),
+ Some(1)
+ );
+
+ cleanup_test_base(&base);
+}
+
#[test]
fn live_handoff_preserves_named_session_socket_paths() {
let _lock = test_lock();