diff --git a/crates/maestro-daemon/src/tmux.rs b/crates/maestro-daemon/src/tmux.rs index c7657c9..89abed6 100644 --- a/crates/maestro-daemon/src/tmux.rs +++ b/crates/maestro-daemon/src/tmux.rs @@ -263,12 +263,22 @@ impl Tmux { } } +/// Phrasings tmux uses for "there is no server on this socket". They vary by +/// version, and `list_sessions` turning one of these into an `Err` stops the +/// liveness watcher reaping for that pass — so a missing phrasing is silent. +const NO_SERVER_PHRASES: &[&str] = &[ + "no server running", + "no sessions", + "error connecting", + // tmux >= 3.7 when the server shuts down while this client is connected, + // which is exactly what killing the last session does. + "server exited unexpectedly", +]; + /// True if tmux's stderr indicates "there is no tmux server running on this /// socket" (multiple phrasings across versions). fn is_no_server_error(stderr: &str) -> bool { - stderr.contains("no server running") - || stderr.contains("no sessions") - || stderr.contains("error connecting") + NO_SERVER_PHRASES.iter().any(|p| stderr.contains(p)) } /// A session pane's process id, foreground command, controlling tty, and @@ -407,6 +417,26 @@ fn check(out: Output) -> Result<(), TmuxError> { mod tests { use super::*; + /// Every phrasing must be recognised: an unmatched one makes + /// `list_sessions` fail, and a failed pass reaps nothing. + #[test] + fn is_no_server_error_matches_every_known_phrasing() { + for phrase in NO_SERVER_PHRASES { + assert!(is_no_server_error(phrase), "unmatched: {phrase}"); + assert!( + is_no_server_error(&format!("{phrase}\n")), + "unmatched with trailing newline: {phrase}" + ); + } + } + + #[test] + fn is_no_server_error_rejects_unrelated_failures() { + assert!(!is_no_server_error("can't find session: maestro-x")); + assert!(!is_no_server_error("usage: kill-session")); + assert!(!is_no_server_error("")); + } + #[test] fn parse_pane_line_splits_pid_command_tty_path() { let p = parse_pane_line("12345 claude /dev/pts/3 /home/naps62/maestro").unwrap();