use super::subscribe::{
is_zellij_pane_closed, numeric_pane_id, parse_zellij_cursor_from_list_panes,
parse_zellij_subscribe_viewport, viewport_to_ansi_chunk,
};
use super::zellij::{CliLiveness, PaneListView, ZellijBackend};
use super::{SpawnOpts, ZELLIJ_PANE_DISCOVERY_MAX_ATTEMPTS, ZELLIJ_PANE_DISCOVERY_RETRY_INTERVAL};
#[test]
fn runtime_config_enables_zellij_web_clients() {
let opts = SpawnOpts {
cwd: "/tmp".to_string(),
cols: 80,
rows: 24,
env: Vec::new(),
};
let (_tmp, config_path, _layout_path) =
ZellijBackend::write_runtime_files("/bin/sh", &[], &opts)
.expect("runtime files should be written");
let config = std::fs::read_to_string(&config_path).expect("config should be readable");
assert!(config.contains("web_server true"));
assert!(config.contains("web_sharing \"on\""));
}
#[test]
fn zellij_pane_discovery_uses_200ms_retry_budget() {
assert_eq!(
ZELLIJ_PANE_DISCOVERY_RETRY_INTERVAL,
std::time::Duration::from_millis(200)
);
assert_eq!(ZELLIJ_PANE_DISCOVERY_MAX_ATTEMPTS, 15);
}
#[test]
fn parse_terminal_pane_id_skips_plugin_panes() {
let panes = br#"[
{"id":0,"is_plugin":true},
{"id":3,"is_plugin":false}
]"#;
assert_eq!(
ZellijBackend::parse_terminal_pane_id(panes),
Some("terminal_3".to_string())
);
}
#[test]
fn parse_terminal_pane_id_returns_none_without_terminal_pane() {
let panes = br#"[
{"id":0,"is_plugin":true},
{"is_plugin":false}
]"#;
assert_eq!(ZellijBackend::parse_terminal_pane_id(panes), None);
assert_eq!(ZellijBackend::parse_terminal_pane_id(b"bad json"), None);
}
#[test]
fn classify_pane_list_treats_empty_and_bad_json_as_unreadable() {
assert_eq!(
ZellijBackend::classify_pane_list(b""),
PaneListView::Unreadable
);
assert_eq!(
ZellijBackend::classify_pane_list(b" \n"),
PaneListView::Unreadable
);
assert_eq!(
ZellijBackend::classify_pane_list(b"bad json"),
PaneListView::Unreadable
);
assert_eq!(
ZellijBackend::classify_pane_list(b"{}"),
PaneListView::Unreadable
);
}
#[test]
fn classify_pane_list_distinguishes_terminal_from_plugin_only() {
let terminal = br#"[{"id":3,"is_plugin":false}]"#;
let plugins_only = br#"[{"id":0,"is_plugin":true}]"#;
let empty_array = b"[]";
assert_eq!(
ZellijBackend::classify_pane_list(terminal),
PaneListView::HasTerminal
);
assert_eq!(
ZellijBackend::classify_pane_list(plugins_only),
PaneListView::NoTerminal
);
assert_eq!(
ZellijBackend::classify_pane_list(empty_array),
PaneListView::NoTerminal
);
}
#[test]
fn decide_cli_liveness_keeps_unknown_and_running_process_alive() {
assert_eq!(
ZellijBackend::decide_cli_liveness(None, None, false).0,
CliLiveness::Alive
);
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), Some(PaneListView::Unreadable), false).0,
CliLiveness::Alive
);
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), None, false).0,
CliLiveness::Alive
);
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), Some(PaneListView::NoTerminal), true).0,
CliLiveness::Alive
);
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), Some(PaneListView::HasTerminal), false).0,
CliLiveness::Alive
);
}
#[test]
fn decide_cli_liveness_incident_empty_panes_with_process_stays_alive() {
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), Some(PaneListView::Unreadable), true).0,
CliLiveness::Alive
);
assert_eq!(
ZellijBackend::classify_pane_list(b""),
PaneListView::Unreadable
);
}
#[test]
fn parse_ps_pid_ppid_command_accepts_bsd_padded_columns() {
let line = " 4183935 501 /opt/homebrew/bin/zellij --server /var/folders/xx/T/zellij-501/beam-abc123";
let parsed = ZellijBackend::parse_ps_pid_ppid_command(line).expect("parse");
assert_eq!(parsed.0, 4183935);
assert_eq!(parsed.1, 501);
assert!(parsed.2.contains("zellij --server"));
}
#[test]
fn parse_ps_pid_ppid_command_accepts_gnu_unpadded_and_rejects_junk() {
let gnu = "1234 1 /usr/bin/zellij --server /run/user/1000/zellij/beam-abc123";
let parsed = ZellijBackend::parse_ps_pid_ppid_command(gnu).expect("parse");
assert_eq!(parsed.0, 1234);
assert_eq!(parsed.1, 1);
assert!(ZellijBackend::parse_ps_pid_ppid_command("").is_none());
assert!(ZellijBackend::parse_ps_pid_ppid_command("not-a-pid 1 zellij").is_none());
assert!(ZellijBackend::parse_ps_pid_ppid_command("12 34").is_none());
}
#[test]
fn command_is_session_server_matches_macos_style_ps_command() {
let cmd = "/opt/homebrew/bin/zellij --server /var/folders/xx/T/zellij-501/beam-abc123";
assert!(ZellijBackend::command_is_session_server(cmd, "beam-abc123"));
assert!(!ZellijBackend::command_is_session_server(cmd, "beam-other"));
let attach = "/opt/homebrew/bin/zellij --session beam-abc123 attach";
assert!(!ZellijBackend::command_is_session_server(
attach,
"beam-abc123"
));
}
#[test]
fn pane_process_from_ps_table_requires_child_of_session_server() {
let table = "\
100 1 /opt/homebrew/bin/zellij --server /tmp/zellij/beam-abc123\n\
200 100 /opt/homebrew/bin/grok --session-id abc --always-approve\n\
300 1 /bin/zsh\n";
assert_eq!(
ZellijBackend::pane_process_from_ps_table("beam-abc123", table),
Some(true)
);
let server_only = " 100 1 /opt/homebrew/bin/zellij --server /tmp/zellij/beam-abc123\n";
assert_eq!(
ZellijBackend::pane_process_from_ps_table("beam-abc123", server_only),
Some(false)
);
assert_eq!(
ZellijBackend::pane_process_from_ps_table("beam-abc123", " 300 1 /bin/zsh\n"),
None
);
let other_parent = "\
100 1 /opt/homebrew/bin/zellij --server /tmp/zellij/beam-abc123\n\
200 999 /opt/homebrew/bin/grok --always-approve\n";
assert_eq!(
ZellijBackend::pane_process_from_ps_table("beam-abc123", other_parent),
Some(false)
);
}
#[test]
fn command_is_session_server_requires_exact_session_name() {
let cmd = "/usr/bin/zellij --server /tmp/zellij/beam-abc123";
assert!(ZellijBackend::command_is_session_server(cmd, "beam-abc123"));
assert!(!ZellijBackend::command_is_session_server(cmd, "beam-abc"));
assert!(!ZellijBackend::command_is_session_server(cmd, "abc123"));
}
#[test]
fn real_ps_table_parses_at_least_one_process() {
let output = std::process::Command::new("ps")
.args(["-axww", "-o", "pid=,ppid=,command="])
.output()
.expect("ps must run on this host");
assert!(output.status.success(), "ps -axww flags must work");
let text = String::from_utf8_lossy(&output.stdout);
assert!(
text.lines()
.filter_map(ZellijBackend::parse_ps_pid_ppid_command)
.count()
> 0,
"ps output should contain parseable pid/ppid/command rows"
);
assert_eq!(
ZellijBackend::pane_process_from_ps_table("beam-session-that-does-not-exist", &text),
None
);
}
#[test]
fn decide_cli_liveness_dead_only_when_session_or_pane_and_process_are_gone() {
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(false), Some(PaneListView::HasTerminal), true).0,
CliLiveness::Dead
);
assert_eq!(
ZellijBackend::decide_cli_liveness(Some(true), Some(PaneListView::NoTerminal), false).0,
CliLiveness::Dead
);
}
#[test]
fn test_numeric_pane_id_valid() {
assert_eq!(numeric_pane_id("terminal_1"), Some(1));
assert_eq!(numeric_pane_id("terminal_0"), Some(0));
assert_eq!(numeric_pane_id("terminal_42"), Some(42));
}
#[test]
fn test_numeric_pane_id_bare_number() {
assert_eq!(numeric_pane_id("1"), Some(1));
assert_eq!(numeric_pane_id("0"), Some(0));
assert_eq!(numeric_pane_id("42"), Some(42));
assert_eq!(numeric_pane_id("999"), Some(999));
}
#[test]
fn test_numeric_pane_id_invalid() {
assert_eq!(numeric_pane_id(""), None);
assert_eq!(numeric_pane_id("terminal_"), None);
assert_eq!(numeric_pane_id("terminal_abc"), None);
assert_eq!(numeric_pane_id("pane_1"), None);
assert_eq!(numeric_pane_id("abc"), None);
}
#[test]
fn parse_subscribe_pane_update_viewport() {
let line = r#"{"event":"pane_update","pane_id":"terminal_1","data":{"viewport":["line1","line2","line3"],"scrollback":[],"is_initial":true}}"#;
let viewport = parse_zellij_subscribe_viewport(line);
assert_eq!(
viewport,
Some(vec![
"line1".to_string(),
"line2".to_string(),
"line3".to_string(),
])
);
}
#[test]
fn parse_subscribe_pane_update_top_level_viewport() {
let line = r#"{"event":"pane_update","pane_id":"terminal_1","viewport":["a","b"],"scrollback":null,"is_initial":true}"#;
let viewport = parse_zellij_subscribe_viewport(line);
assert_eq!(viewport, Some(vec!["a".to_string(), "b".to_string()]));
}
#[test]
fn parse_subscribe_pane_update_empty_viewport() {
let line = r#"{"event":"pane_update","pane_id":"terminal_1","data":{"viewport":[],"scrollback":[],"is_initial":true}}"#;
let viewport = parse_zellij_subscribe_viewport(line);
assert_eq!(viewport, Some(vec![]));
}
#[test]
fn parse_subscribe_pane_update_with_ansi() {
let line = r#"{"event":"pane_update","pane_id":"terminal_1","data":{"viewport":["\u001b[32mgreen\u001b[0m","normal"],"scrollback":[],"is_initial":false}}"#;
let viewport = parse_zellij_subscribe_viewport(line);
assert_eq!(
viewport,
Some(vec![
"\u{1b}[32mgreen\u{1b}[0m".to_string(),
"normal".to_string(),
])
);
}
#[test]
fn parse_subscribe_pane_closed_returns_none() {
let line = r#"{"event":"pane_closed","pane_id":"terminal_1"}"#;
assert_eq!(parse_zellij_subscribe_viewport(line), None);
}
#[test]
fn parse_subscribe_unknown_event() {
let line = r#"{"event":"session_closed"}"#;
assert_eq!(parse_zellij_subscribe_viewport(line), None);
}
#[test]
fn parse_subscribe_invalid_json() {
assert_eq!(parse_zellij_subscribe_viewport("not json"), None);
assert_eq!(parse_zellij_subscribe_viewport(""), None);
}
#[test]
fn test_is_zellij_pane_closed_true() {
assert!(is_zellij_pane_closed(
r#"{"event":"pane_closed","pane_id":"terminal_1"}"#
));
}
#[test]
fn test_is_zellij_pane_closed_false() {
assert!(!is_zellij_pane_closed(
r#"{"event":"pane_update","pane_id":"terminal_1","data":{}}"#
));
assert!(!is_zellij_pane_closed("not json"));
assert!(!is_zellij_pane_closed(""));
}
#[test]
fn viewport_to_ansi_basic() {
let viewport = vec!["hello".to_string(), "world".to_string()];
let chunk = viewport_to_ansi_chunk(&viewport);
assert!(chunk.contains("\x1b[H"), "should contain home");
assert!(chunk.contains("\x1b[2J"), "should contain clear screen");
assert!(chunk.contains("\x1b[?25l"), "should hide cursor");
assert!(chunk.contains("\x1b[?25h"), "should show cursor");
assert!(
chunk.contains("hello\r\nworld"),
"should join lines with CRLF"
);
}
#[test]
fn viewport_to_ansi_no_trailing_crlf() {
let viewport = vec!["line1".to_string()];
let chunk = viewport_to_ansi_chunk(&viewport);
assert!(!chunk.ends_with("\r\n"), "must not trail with CRLF");
assert!(!chunk.ends_with('\n'), "must not trail with LF");
assert!(
chunk.ends_with("line1\x1b[?25h"),
"should end with last line + show cursor"
);
}
#[test]
fn viewport_to_ansi_multiline_no_trailing_crlf() {
let viewport = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let chunk = viewport_to_ansi_chunk(&viewport);
assert!(!chunk.ends_with("\r\n"));
assert!(!chunk.ends_with('\n'));
assert!(chunk.contains("a\r\nb\r\nc"));
}
#[test]
fn viewport_to_ansi_empty() {
assert_eq!(viewport_to_ansi_chunk(&[]), "");
}
#[test]
fn viewport_to_ansi_preserves_ansi_content() {
let viewport = vec![
"\u{1b}[32mgreen\u{1b}[0m text".to_string(),
"\u{1b}[1mbold\u{1b}[0m".to_string(),
];
let chunk = viewport_to_ansi_chunk(&viewport);
assert!(chunk.contains("\u{1b}[32mgreen\u{1b}[0m text"));
assert!(chunk.contains("\u{1b}[1mbold\u{1b}[0m"));
}
#[test]
fn parse_zellij_cursor_single_pane() {
let json = r#"[
{"id":1,"is_plugin":false,"cursor_coordinates_in_pane":{"x":10,"y":5}}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 1), Some((10, 5)));
}
#[test]
fn parse_zellij_cursor_array_format() {
let json = r#"[
{"id":1,"cursor_coordinates_in_pane":[3, 7]}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 1), Some((3, 7)));
}
#[test]
fn parse_zellij_cursor_array_zero() {
let json = r#"[
{"id":1,"cursor_coordinates_in_pane":[0, 0]}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 1), Some((0, 0)));
}
#[test]
fn parse_zellij_cursor_multiple_panes() {
let json = r#"[
{"id":1,"is_plugin":false,"cursor_coordinates_in_pane":{"x":0,"y":0}},
{"id":2,"is_plugin":false,"cursor_coordinates_in_pane":{"x":80,"y":24}},
{"id":3,"is_plugin":true}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 1), Some((0, 0)));
assert_eq!(parse_zellij_cursor_from_list_panes(json, 2), Some((80, 24)));
assert_eq!(parse_zellij_cursor_from_list_panes(json, 3), None);
}
#[test]
fn parse_zellij_cursor_pane_not_found() {
let json = r#"[
{"id":1,"cursor_coordinates_in_pane":{"x":10,"y":5}}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 2), None);
}
#[test]
fn parse_zellij_cursor_missing_field() {
assert_eq!(parse_zellij_cursor_from_list_panes(r#"[]"#, 1), None);
assert_eq!(
parse_zellij_cursor_from_list_panes(r#"[{"id":1}]"#, 1),
None
);
assert_eq!(parse_zellij_cursor_from_list_panes("bad json", 1), None);
}
#[test]
fn parse_zellij_cursor_zero_coordinates() {
let json = r#"[
{"id":1,"cursor_coordinates_in_pane":{"x":0,"y":0}}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 1), Some((0, 0)));
}
#[test]
fn parse_zellij_cursor_id_as_number_in_json() {
let json = r#"[
{"id":42,"cursor_coordinates_in_pane":{"x":5,"y":3}}
]"#;
assert_eq!(parse_zellij_cursor_from_list_panes(json, 42), Some((5, 3)));
}
#[test]
fn dump_screen_viewport_args_no_full_flag() {
let args = ZellijBackend::dump_screen_viewport_args("pane_1");
assert_eq!(
args,
vec![
"dump-screen".to_string(),
"--ansi".to_string(),
"--pane-id".to_string(),
"pane_1".to_string(),
]
);
assert!(!args.contains(&"--full".to_string()));
}
#[test]
fn dump_screen_viewport_args_different_pane_ids() {
let args = ZellijBackend::dump_screen_viewport_args("terminal_99");
assert_eq!(args[3], "terminal_99");
assert!(args.contains(&"--ansi".to_string()));
assert!(!args.contains(&"--full".to_string()));
}
#[test]
fn dump_screen_viewport_args_no_full_through_observe_path() {
let args = ZellijBackend::dump_screen_viewport_args("observe_pane");
assert_eq!(args[0], "dump-screen");
assert_eq!(args[1], "--ansi");
assert_eq!(args[2], "--pane-id");
assert_eq!(args[3], "observe_pane");
assert!(!args.contains(&"--full".to_string()));
}