aboutsummaryrefslogtreecommitdiff
path: root/tests/multi_client.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-04 15:37:33 +0300
committerGitHub <noreply@github.com>2026-08-04 15:37:33 +0300
commite6fa5289f2de03bc7babf2ec07efef6b45ed398f (patch)
tree9d4263ec646ff9e6d281f046529ccf16552e9fe4 /tests/multi_client.rs
parentadb50cba9b15583db019bb655119915869e8c44e (diff)
fix(ui): preserve sidebar scroll across clients (#2280)
refs #2255 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'tests/multi_client.rs')
-rw-r--r--tests/multi_client.rs91
1 files changed, 91 insertions, 0 deletions
diff --git a/tests/multi_client.rs b/tests/multi_client.rs
index 6e367eaf..20deedaf 100644
--- a/tests/multi_client.rs
+++ b/tests/multi_client.rs
@@ -273,6 +273,19 @@ fn create_workspace_and_root_pane(socket_path: &Path, label: &str) -> (String, S
(workspace_id, pane_id)
}
+fn report_idle_agent(socket_path: &Path, pane_id: &str) {
+ let response = send_json_request(
+ socket_path,
+ &format!(
+ r#"{{"id":"report_agent","method":"pane.report_agent","params":{{"pane_id":"{pane_id}","agent":"pi","state":"idle","source":"multi-client-test"}}}}"#
+ ),
+ );
+ assert!(
+ response.get("error").is_none(),
+ "pane.report_agent should succeed: {response}"
+ );
+}
+
fn pane_send_input(socket_path: &Path, pane_id: &str, text: &str) {
let request = format!(
"{{\"id\":\"send_input\",\"method\":\"pane.send_input\",\"params\":{{\"pane_id\":\"{pane_id}\",\"text\":\"{}\",\"keys\":[\"Enter\"]}}}}",
@@ -747,6 +760,15 @@ fn frame_contains_text(frame: &FrameWire, needle: &str) -> bool {
frame_text(frame).contains(needle)
}
+fn agent_panel_starts_with(frame: &FrameWire, agent_label: &str) -> bool {
+ frame_text(frame)
+ .lines()
+ .skip_while(|line| !line.contains("agents"))
+ .skip(1)
+ .find(|line| line.contains("agent-"))
+ .is_some_and(|line| line.contains(agent_label))
+}
+
#[test]
fn multi_client_allows_multiple_simultaneous_connections() {
let _lock = test_lock();
@@ -828,6 +850,75 @@ fn multi_client_effective_size_shrinks_when_smaller_client_joins() {
}
#[test]
+fn non_foreground_client_render_preserves_agent_panel_scroll() {
+ 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 = spawn_server(&config_home, &runtime_dir, &api_socket);
+ wait_for_socket(&api_socket, Duration::from_secs(10));
+ wait_for_file(&client_socket, Duration::from_secs(10));
+
+ for index in 1..=23 {
+ let (_, pane_id) =
+ create_workspace_and_root_pane(&api_socket, &format!("agent-{index:02}"));
+ report_idle_agent(&api_socket, &pane_id);
+ }
+
+ let mut setup_client = connect_raw_client(&client_socket, 106, 40);
+ assert!(wait_for_frame(&mut setup_client, Duration::from_secs(2)));
+ drain_server_messages(&mut setup_client, Duration::from_millis(250));
+
+ let wheel_down = b"\x1b[<65;10;30M";
+ send_client_input(&mut setup_client, &wheel_down.repeat(20));
+ let (reached_bottom, setup_frames) = wait_for_frame_matching_with_snapshots(
+ &mut setup_client,
+ Duration::from_secs(3),
+ |frame| agent_panel_starts_with(frame, "agent-16"),
+ )
+ .expect("setup frame decoding should succeed");
+ assert!(
+ reached_bottom,
+ "40-row client should scroll the agent panel to its final page; frames:\n{}",
+ setup_frames.join("\n--- frame ---\n")
+ );
+ send_client_detach(&mut setup_client);
+ drop(setup_client);
+
+ let mut tall_background = connect_raw_client(&client_socket, 106, 64);
+ assert!(wait_for_frame(&mut tall_background, Duration::from_secs(2)));
+ let mut probe = connect_raw_client(&client_socket, 106, 40);
+ let (started_at_tall_limit, initial_frames) =
+ wait_for_frame_matching_with_snapshots(&mut probe, Duration::from_secs(3), |frame| {
+ agent_panel_starts_with(frame, "agent-10")
+ })
+ .expect("initial probe frame decoding should succeed");
+ assert!(
+ started_at_tall_limit,
+ "tall client should normalize the shared scroll before the probe attaches; frames:\n{}",
+ initial_frames.join("\n--- frame ---\n")
+ );
+ drain_server_messages(&mut probe, Duration::from_millis(250));
+
+ send_client_input(&mut probe, wheel_down);
+ let (scrolled, probe_frames) =
+ wait_for_frame_matching_with_snapshots(&mut probe, Duration::from_secs(3), |frame| {
+ agent_panel_starts_with(frame, "agent-11")
+ })
+ .expect("probe frame decoding should succeed");
+ assert!(
+ scrolled,
+ "background client projection must not undo the foreground wheel event; frames:\n{}",
+ probe_frames.join("\n--- frame ---\n")
+ );
+
+ cleanup_spawned_herdr(server, base);
+}
+
+#[test]
fn multi_client_broadcasts_frame_updates_to_all_clients() {
let _lock = test_lock();
let base = unique_test_dir();