aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/client_mode.rs283
1 files changed, 276 insertions, 7 deletions
diff --git a/tests/client_mode.rs b/tests/client_mode.rs
index c92a62d1..e79375cf 100644
--- a/tests/client_mode.rs
+++ b/tests/client_mode.rs
@@ -113,20 +113,72 @@ fn spawn_client_process(
}
}
+fn spawn_no_session_process(config_home: &PathBuf, runtime_dir: &PathBuf) -> SpawnedHerdr {
+ fs::create_dir_all(config_home.join(app_dir_name())).unwrap();
+ fs::create_dir_all(runtime_dir).unwrap();
+ register_runtime_dir(runtime_dir);
+ fs::write(
+ config_home.join(app_dir_name()).join("config.toml"),
+ "onboarding = false\n[ui]\nwindow_title = \"monolithic\"\n",
+ )
+ .unwrap();
+
+ let pair = native_pty_system()
+ .openpty(PtySize {
+ rows: 24,
+ cols: 80,
+ pixel_width: 0,
+ pixel_height: 0,
+ })
+ .unwrap();
+ let mut cmd = CommandBuilder::new(env!("CARGO_BIN_EXE_herdr"));
+ cmd.arg("--no-session");
+ cmd.env("XDG_CONFIG_HOME", config_home);
+ cmd.env("XDG_RUNTIME_DIR", runtime_dir);
+ cmd.env("SHELL", "/bin/sh");
+ cmd.env_remove("HERDR_ENV");
+ cmd.env_remove("HERDR_SOCKET_PATH");
+ cmd.env_remove("HERDR_CLIENT_SOCKET_PATH");
+ cmd.env_remove("HERDR_SESSION");
+ cmd.env_remove("HERDR_WORKSPACE_ID");
+ cmd.env_remove("HERDR_TAB_ID");
+ cmd.env_remove("HERDR_PANE_ID");
+ let child = pair.slave.spawn_command(cmd).unwrap();
+ register_spawned_herdr_pid(child.process_id());
+ drop(pair.slave);
+
+ SpawnedHerdr {
+ _master: Some(pair.master),
+ child,
+ }
+}
+
fn spawn_server(
config_home: &PathBuf,
runtime_dir: &PathBuf,
api_socket_path: &PathBuf,
+ client_socket_path: &PathBuf,
+) -> SpawnedHerdr {
+ spawn_server_with_config(
+ config_home,
+ runtime_dir,
+ api_socket_path,
+ client_socket_path,
+ "onboarding = false\n",
+ )
+}
+
+fn spawn_server_with_config(
+ config_home: &PathBuf,
+ runtime_dir: &PathBuf,
+ api_socket_path: &PathBuf,
_client_socket_path: &PathBuf,
+ config: &str,
) -> SpawnedHerdr {
- fs::create_dir_all(config_home.join("herdr")).unwrap();
+ fs::create_dir_all(config_home.join(app_dir_name())).unwrap();
fs::create_dir_all(runtime_dir).unwrap();
register_runtime_dir(runtime_dir);
- fs::write(
- config_home.join("herdr/config.toml"),
- "onboarding = false\n",
- )
- .unwrap();
+ fs::write(config_home.join(app_dir_name()).join("config.toml"), config).unwrap();
let pair = native_pty_system()
.openpty(PtySize {
@@ -647,7 +699,24 @@ fn attach_thin_client(
api_socket: &PathBuf,
client_socket: &PathBuf,
) -> (SpawnedHerdr, SpawnedHerdr, SharedOutput) {
- let spawned_server = spawn_server(config_home, runtime_dir, api_socket, client_socket);
+ attach_thin_client_with_config(
+ config_home,
+ runtime_dir,
+ api_socket,
+ client_socket,
+ "onboarding = false\n",
+ )
+}
+
+fn attach_thin_client_with_config(
+ config_home: &PathBuf,
+ runtime_dir: &PathBuf,
+ api_socket: &PathBuf,
+ client_socket: &PathBuf,
+ config: &str,
+) -> (SpawnedHerdr, SpawnedHerdr, SharedOutput) {
+ let spawned_server =
+ spawn_server_with_config(config_home, runtime_dir, api_socket, client_socket, config);
wait_for_socket(api_socket, Duration::from_secs(10));
wait_for_socket(client_socket, Duration::from_secs(10));
@@ -686,6 +755,206 @@ fn attach_thin_client(
(spawned_server, thin_client, output)
}
+fn captured_window_titles(output: &SharedOutput) -> Vec<String> {
+ read_output(output)
+ .split("\x1b]0;")
+ .skip(1)
+ .filter_map(|suffix| {
+ suffix
+ .split_once('\x07')
+ .map(|(title, _)| title.to_string())
+ })
+ .collect()
+}
+
+fn wait_for_window_title(output: &SharedOutput, expected_suffix: &str) -> String {
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while Instant::now() < deadline {
+ if let Some(title) = captured_window_titles(output)
+ .into_iter()
+ .find(|title| title.ends_with(expected_suffix))
+ {
+ return title;
+ }
+ thread::sleep(Duration::from_millis(20));
+ }
+ panic!(
+ "outer window title ending in {expected_suffix:?} was not emitted; titles: {:?}; output: {:?}",
+ captured_window_titles(output),
+ read_output(output)
+ );
+}
+
+fn wait_for_pane_terminal_title(socket_path: &PathBuf, pane_id: &str, expected: &str) {
+ let deadline = Instant::now() + Duration::from_secs(5);
+ while Instant::now() < deadline {
+ let request = serde_json::json!({
+ "id": "window-title-pane-get",
+ "method": "pane.get",
+ "params": {"pane_id": pane_id},
+ });
+ let response = send_json_request(socket_path, &request.to_string());
+ if response["result"]["pane"]["terminal_title"].as_str() == Some(expected) {
+ return;
+ }
+ thread::sleep(Duration::from_millis(20));
+ }
+ panic!("pane {pane_id} did not report terminal title {expected:?}");
+}
+
+fn send_pane_shell_command(socket_path: &PathBuf, pane_id: &str, command: &str) {
+ let request = serde_json::json!({
+ "id": "window-title-command",
+ "method": "pane.send_input",
+ "params": {
+ "pane_id": pane_id,
+ "text": command,
+ "keys": ["Enter"],
+ }
+ });
+ let response = send_json_request(socket_path, &request.to_string());
+ assert_eq!(response["result"]["type"], "ok", "{response}");
+}
+
+#[test]
+fn configured_window_title_is_emitted_in_no_session_mode() {
+ let _lock = test_lock();
+ let base = unique_test_dir();
+ let config_home = base.join("config");
+ let runtime_dir = base.join("runtime");
+ let no_session = spawn_no_session_process(&config_home, &runtime_dir);
+ let reader = no_session
+ ._master
+ .as_ref()
+ .expect("no-session PTY master")
+ .try_clone_reader()
+ .expect("clone no-session PTY reader");
+ let output = spawn_pty_drain(reader);
+
+ wait_for_window_title(&output, "monolithic");
+
+ cleanup_spawned_herdr(no_session, base);
+}
+
+#[test]
+fn configured_window_title_tracks_all_tokens_and_focused_osc_only() {
+ 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 client_socket = runtime_dir.join("herdr-client.sock");
+ let (server, client, output) = attach_thin_client_with_config(
+ &config_home,
+ &runtime_dir,
+ &api_socket,
+ &client_socket,
+ "onboarding = false\n[ui]\nwindow_title = \"H={hostname}|W={workspace}|T={tab}|P={pane}|O={terminal_title}\"\n",
+ );
+
+ let created = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "create-workspace",
+ "method": "workspace.create",
+ "params": {"cwd": base, "focus": true},
+ })
+ .to_string(),
+ );
+ assert_eq!(created["result"]["type"], "workspace_created", "{created}");
+ let workspace_id = created["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .expect("workspace id")
+ .to_string();
+ let pane_id = created["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("pane id")
+ .to_string();
+ let tab_id = created["result"]["tab"]["tab_id"]
+ .as_str()
+ .expect("tab id")
+ .to_string();
+
+ for request in [
+ serde_json::json!({
+ "id": "rename-workspace",
+ "method": "workspace.rename",
+ "params": {"workspace_id": workspace_id, "label": "space-a"},
+ }),
+ serde_json::json!({
+ "id": "rename-tab",
+ "method": "tab.rename",
+ "params": {"tab_id": tab_id, "label": "tab-a"},
+ }),
+ serde_json::json!({
+ "id": "rename-pane",
+ "method": "pane.rename",
+ "params": {"pane_id": pane_id, "label": "pane-a"},
+ }),
+ ] {
+ let response = send_json_request(&api_socket, &request.to_string());
+ assert!(response.get("result").is_some(), "{response}");
+ }
+
+ let renamed = wait_for_window_title(&output, "|W=space-a|T=tab-a|P=pane-a|O=");
+ assert!(renamed.starts_with("H="));
+ assert!(
+ !renamed.starts_with("H=|"),
+ "hostname token was empty: {renamed}"
+ );
+
+ send_pane_shell_command(&api_socket, &pane_id, r"printf '\033]0;⠋ building\007'");
+ wait_for_window_title(&output, "|W=space-a|T=tab-a|P=pane-a|O=building");
+
+ let second_tab = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "second-tab",
+ "method": "tab.create",
+ "params": {"workspace_id": workspace_id, "focus": true},
+ })
+ .to_string(),
+ );
+ assert_eq!(second_tab["result"]["type"], "tab_created", "{second_tab}");
+ let second_pane_id = second_tab["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("second pane id")
+ .to_string();
+ wait_for_window_title(&output, "|W=space-a|T=2|P=|O=");
+ let titles_before_hidden_update = captured_window_titles(&output).len();
+ send_pane_shell_command(&api_socket, &pane_id, r"printf '\033]0;hidden update\007'");
+ // Intentionally consume the AppState title through a read-only request
+ // before the queued source is handled.
+ wait_for_pane_terminal_title(&api_socket, &pane_id, "hidden update");
+ send_pane_shell_command(
+ &api_socket,
+ &second_pane_id,
+ r"printf '\033]0;foreground marker\007'",
+ );
+ wait_for_window_title(&output, "|W=space-a|T=2|P=|O=foreground marker");
+ assert!(
+ captured_window_titles(&output)[titles_before_hidden_update..]
+ .iter()
+ .all(|title| !title.ends_with("|O=hidden update")),
+ "a hidden pane title reached the outer terminal"
+ );
+
+ let focused = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "focus-first-tab",
+ "method": "tab.focus",
+ "params": {"tab_id": tab_id},
+ })
+ .to_string(),
+ );
+ assert_eq!(focused["result"]["tab"]["focused"], true, "{focused}");
+ wait_for_window_title(&output, "|W=space-a|T=tab-a|P=pane-a|O=hidden update");
+
+ drop(server);
+ cleanup_spawned_herdr(client, base);
+}
+
/// Polls until the client exits, then returns only the output captured after
/// the `since` byte watermark. Panics if the client does not exit within the
/// deadline.