aboutsummaryrefslogtreecommitdiff
path: root/tests/client_mode.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/client_mode.rs')
-rw-r--r--tests/client_mode.rs527
1 files changed, 255 insertions, 272 deletions
diff --git a/tests/client_mode.rs b/tests/client_mode.rs
index 5112182d..c6c76c34 100644
--- a/tests/client_mode.rs
+++ b/tests/client_mode.rs
@@ -2,7 +2,7 @@
#![cfg(unix)]
-mod support;
+pub mod support;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
@@ -13,12 +13,13 @@ use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
-use serde::Deserialize;
use serde_json::Value;
use support::{
- cleanup_test_base, client_handshake, encode_varint_u32, frame_message, read_server_message,
- register_runtime_dir, register_spawned_herdr_pid, unregister_spawned_herdr_pid,
- wait_for_message_variant, wait_for_socket, wait_until, CURRENT_PROTOCOL,
+ cleanup_test_base, client_shell_handshake, read_server_message, register_runtime_dir,
+ register_spawned_herdr_pid, unregister_spawned_herdr_pid, wait_for_client_shell_bootstrap,
+ wait_for_message_variant, wait_for_message_variants, wait_for_socket, wait_until,
+ CURRENT_PROTOCOL, SERVER_MESSAGE_PANE_SURFACE, SERVER_MESSAGE_PANE_SURFACE_PATCH,
+ SERVER_MESSAGE_SEMANTIC_NOTIFICATION, SERVER_MESSAGE_SERVER_SHUTDOWN,
};
fn unique_test_dir() -> PathBuf {
@@ -86,12 +87,30 @@ fn spawn_client_process(
spawn_client_process_with_args(config_home, runtime_dir, api_socket_path, &["client"])
}
+fn spawn_client_shell_process(
+ config_home: &PathBuf,
+ runtime_dir: &PathBuf,
+ api_socket_path: &PathBuf,
+) -> SpawnedHerdr {
+ spawn_client_process_with_args(config_home, runtime_dir, api_socket_path, &["client"])
+}
+
fn spawn_client_process_with_args(
config_home: &PathBuf,
runtime_dir: &PathBuf,
api_socket_path: &PathBuf,
args: &[&str],
) -> SpawnedHerdr {
+ spawn_client_process_with_args_and_env(config_home, runtime_dir, api_socket_path, args, &[])
+}
+
+fn spawn_client_process_with_args_and_env(
+ config_home: &PathBuf,
+ runtime_dir: &PathBuf,
+ api_socket_path: &PathBuf,
+ args: &[&str],
+ extra_env: &[(&str, &str)],
+) -> SpawnedHerdr {
register_runtime_dir(runtime_dir);
let pair = native_pty_system()
.openpty(PtySize {
@@ -111,47 +130,10 @@ fn spawn_client_process_with_args(
cmd.env_remove("HERDR_CLIENT_SOCKET_PATH");
cmd.env("SHELL", "/bin/sh");
cmd.env_remove("HERDR_ENV");
-
- let child = pair.slave.spawn_command(cmd).unwrap();
- register_spawned_herdr_pid(child.process_id());
- drop(pair.slave);
-
- SpawnedHerdr {
- _master: Some(pair.master),
- child,
+ for (key, value) in extra_env {
+ cmd.env(key, value);
}
-}
-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);
@@ -266,100 +248,12 @@ fn app_dir_name() -> &'static str {
}
}
-#[allow(dead_code)]
-#[derive(Debug, Deserialize)]
-struct FrameWire {
- cells: Vec<CellWire>,
- width: u16,
- height: u16,
- cursor: Option<CursorWire>,
- hyperlinks: Vec<String>,
- graphics: Vec<u8>,
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Deserialize)]
-struct CellWire {
- symbol: String,
- fg: u32,
- bg: u32,
- modifier: u16,
- skip: bool,
- hyperlink: Option<u32>,
-}
-
-#[derive(Debug, Deserialize)]
-struct CursorWire {
- x: u16,
- y: u16,
- visible: bool,
- shape: u8,
-}
-
-fn decode_frame_payload(payload: &[u8]) -> std::io::Result<FrameWire> {
- bincode::serde::decode_from_slice(payload, bincode::config::standard())
- .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string()))
- .and_then(|(frame, consumed): (FrameWire, usize)| {
- if consumed != payload.len() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- format!(
- "frame payload had trailing bytes: consumed={}, len={}",
- consumed,
- payload.len()
- ),
- ));
- }
- Ok(frame)
- })
-}
-
-fn read_next_frame_payload(stream: &mut UnixStream, timeout: Duration) -> Result<Vec<u8>, String> {
- stream
- .set_read_timeout(Some(Duration::from_millis(200)))
- .map_err(|e| e.to_string())?;
- let deadline = Instant::now() + timeout;
- while Instant::now() < deadline {
- match read_server_message(stream) {
- Ok((1, payload)) => return Ok(payload),
- Ok(_) => continue,
- Err(_) => continue,
- }
- }
- Err("timed out waiting for Frame message".into())
-}
-
-fn frame_text(frame: &FrameWire) -> String {
- if frame.cells.is_empty() {
- return String::new();
- }
-
- let width = frame.width.max(1) as usize;
- let mut text = String::new();
- for row in frame.cells.chunks(width) {
- for cell in row {
- let _ = (cell.fg, cell.bg, cell.modifier, cell.skip);
- text.push_str(&cell.symbol);
- }
- text.push('\n');
- }
- let _ = (frame.height, frame.graphics.len());
- if let Some(cursor) = frame.cursor.as_ref() {
- let _ = (cursor.x, cursor.y, cursor.visible, cursor.shape);
- }
-
- text
-}
-
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[test]
-fn client_connects_and_receives_frame() {
- // Client connects to server and handshake completes.
- // Client receives Frame messages.
- // Server sends rendered frames to connected clients.
+fn client_connects_and_receives_pane_surface() {
let _lock = test_lock();
let base = unique_test_dir();
let config_home = base.join("config");
@@ -371,22 +265,13 @@ fn client_connects_and_receives_frame() {
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_socket(&client_socket, Duration::from_secs(10));
- // Connect and handshake.
let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket");
- let (version, error) =
- client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed");
- assert_eq!(
- version, CURRENT_PROTOCOL,
- "server should report current protocol version"
- );
- assert!(
- error.is_none(),
- "handshake should not have error: {:?}",
- error
- );
-
- read_next_frame_payload(&mut stream, Duration::from_secs(10))
- .expect("should receive a frame from server");
+ let (version, error) = client_shell_handshake(&mut stream, CURRENT_PROTOCOL, 54, 23)
+ .expect("handshake should succeed");
+ assert_eq!(version, CURRENT_PROTOCOL);
+ assert!(error.is_none(), "{error:?}");
+ wait_for_client_shell_bootstrap(&mut stream, Duration::from_secs(10))
+ .expect("should receive the shell snapshot and pane surface");
cleanup_spawned_herdr(spawned, base);
}
@@ -560,41 +445,26 @@ fn client_sees_headless_startup_config_diagnostic() {
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_socket(&client_socket, Duration::from_secs(10));
- let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket");
- let (version, error) =
- client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed");
- assert_eq!(version, CURRENT_PROTOCOL);
- assert!(error.is_none(), "{:?}", error);
-
- stream
- .set_read_timeout(Some(Duration::from_secs(5)))
- .unwrap();
- let deadline = Instant::now() + Duration::from_secs(5);
- let mut found_diagnostic = false;
- let mut last_frame_text = String::new();
- while Instant::now() < deadline {
- match read_server_message(&mut stream) {
- Ok((1, payload)) => {
- let frame = decode_frame_payload(&payload).expect("decode frame");
- last_frame_text = frame_text(&frame);
- if last_frame_text.contains("config.toml")
- && last_frame_text.contains("herdr config check")
- {
- found_diagnostic = true;
- break;
- }
- }
- Ok(_) => {}
- Err(_) => break,
- }
- }
-
+ let client = spawn_client_shell_process(&config_home, &runtime_dir, &api_socket);
+ let output = spawn_pty_drain(
+ client
+ ._master
+ .as_ref()
+ .expect("client shell master")
+ .try_clone_reader()
+ .expect("clone client shell reader"),
+ );
assert!(
- found_diagnostic,
- "attached client should see startup config parse diagnostic; last frame:\n{last_frame_text}"
+ wait_until(Duration::from_secs(8), Duration::from_millis(20), || {
+ let output = read_output(&output);
+ output.contains("config.toml") && output.contains("herdr config check")
+ }),
+ "client shell should render startup config diagnostic; output: {:?}",
+ read_output(&output)
);
- cleanup_spawned_herdr(spawned, base);
+ drop(spawned);
+ cleanup_spawned_herdr(client, base);
}
#[test]
@@ -881,6 +751,120 @@ fn attach_thin_client_with_config(
(spawned_server, thin_client, output)
}
+#[test]
+fn client_shell_detaches_restores_and_freshly_reattaches_to_current_state() {
+ 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 mut server = spawn_server(&config_home, &runtime_dir, &api_socket, &client_socket);
+ wait_for_socket(&api_socket, Duration::from_secs(10));
+ wait_for_socket(&client_socket, Duration::from_secs(10));
+
+ let created = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "client-shell-lifecycle-workspace",
+ "method": "workspace.create",
+ "params": {"cwd": base, "focus": true, "label": "shell-lifecycle"},
+ })
+ .to_string(),
+ );
+ assert_eq!(created["result"]["type"], "workspace_created", "{created}");
+ let pane_id = created["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("root pane id")
+ .to_string();
+ send_pane_shell_command(&api_socket, &pane_id, "printf 'SHELL_LIFECYCLE_INITIAL\\n'");
+
+ let mut client_a = spawn_client_shell_process(&config_home, &runtime_dir, &api_socket);
+ let output_a = spawn_pty_drain(
+ client_a
+ ._master
+ .as_ref()
+ .expect("first client shell PTY")
+ .try_clone_reader()
+ .expect("clone first client shell reader"),
+ );
+ assert!(
+ wait_until(Duration::from_secs(8), Duration::from_millis(20), || {
+ let output = read_output(&output_a);
+ output.contains("shell-lifecycle") && output.contains("SHELL_LIFECYCLE_INITIAL")
+ }),
+ "client shell should compose one coherent snapshot and pane surface; output: {:?}",
+ read_output(&output_a)
+ );
+
+ let detach_watermark = output_len(&output_a);
+ client_a
+ ._master
+ .as_ref()
+ .expect("first client shell PTY")
+ .take_writer()
+ .expect("first client shell writer")
+ .write_all(b"\x02q")
+ .expect("detach first client shell");
+ let detach_output = drain_until_client_exits(&mut client_a, &output_a, detach_watermark);
+ assert!(
+ output_has_mouse_teardown(&detach_output),
+ "client shell should restore the host terminal after detach; output: {detach_output:?}"
+ );
+ assert!(
+ ping_socket(&api_socket).contains("pong"),
+ "server should remain alive after client shell detach"
+ );
+ drop(client_a);
+
+ send_pane_shell_command(
+ &api_socket,
+ &pane_id,
+ "printf 'SHELL_LIFECYCLE_DETACHED\\n'",
+ );
+ let mut client_b = spawn_client_shell_process(&config_home, &runtime_dir, &api_socket);
+ let output_b = spawn_pty_drain(
+ client_b
+ ._master
+ .as_ref()
+ .expect("reattached client shell PTY")
+ .try_clone_reader()
+ .expect("clone reattached client shell reader"),
+ );
+ assert!(
+ wait_until(Duration::from_secs(8), Duration::from_millis(20), || {
+ let output = read_output(&output_b);
+ output.contains("shell-lifecycle") && output.contains("SHELL_LIFECYCLE_DETACHED")
+ }),
+ "fresh client shell should receive current state and detached-period output; output: {:?}",
+ read_output(&output_b)
+ );
+
+ let disconnect_watermark = output_len(&output_b);
+ if let Some(pid) = server.child.process_id() {
+ unsafe {
+ libc::kill(pid as libc::pid_t, libc::SIGKILL);
+ }
+ }
+ server.close_master();
+ let disconnect_output =
+ drain_until_client_exits(&mut client_b, &output_b, disconnect_watermark);
+ assert!(
+ output_has_mouse_teardown(&disconnect_output),
+ "client shell should restore the host terminal after endpoint loss; output: {disconnect_output:?}"
+ );
+ assert!(
+ disconnect_output
+ .to_lowercase()
+ .contains("lost connection to server"),
+ "client shell should explain endpoint loss; output: {disconnect_output:?}"
+ );
+
+ drop(server);
+ cleanup_spawned_herdr(client_b, base);
+}
+
fn captured_window_titles(output: &SharedOutput) -> Vec<String> {
read_output(output)
.split("\x1b]0;")
@@ -943,26 +927,6 @@ fn send_pane_shell_command(socket_path: &PathBuf, pane_id: &str, command: &str)
}
#[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();
@@ -1029,7 +993,7 @@ fn configured_window_title_tracks_all_tokens_and_focused_osc_only() {
"hostname token was empty: {renamed}"
);
- send_pane_shell_command(&api_socket, &pane_id, r"printf '\033]0;⠋ building\007'");
+ 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(
@@ -1308,14 +1272,7 @@ fn client_exits_cleanly_when_terminal_hangs_up() {
}
#[test]
-fn client_receives_frame_after_pane_output() {
- // End-to-end test: server renders, client receives Frame.
- // This test verifies the full flow:
- // 1. Start server
- // 2. Connect client, handshake
- // 3. Send input to pane (echo command)
- // 4. Wait for a new frame from the server
- // 5. Verify the frame contains the pane output
+fn client_receives_pane_surface_after_pane_output() {
let _lock = test_lock();
let base = unique_test_dir();
let config_home = base.join("config");
@@ -1327,33 +1284,55 @@ fn client_receives_frame_after_pane_output() {
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_socket(&client_socket, Duration::from_secs(10));
- // Connect and handshake.
let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket");
- let (version, error) =
- client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed");
+ let (version, error) = client_shell_handshake(&mut stream, CURRENT_PROTOCOL, 54, 23)
+ .expect("handshake should succeed");
assert_eq!(version, CURRENT_PROTOCOL);
- assert!(error.is_none(), "{:?}", error);
-
- read_next_frame_payload(&mut stream, Duration::from_secs(10))
- .expect("should receive initial frame");
-
- // Send input to trigger a state change and re-render.
- let input_data = b"echo test-output\n".to_vec();
- let input_payload = {
- let mut buf = encode_varint_u32(1); // Input variant
- buf.extend_from_slice(&encode_varint_u32(input_data.len() as u32));
- buf.extend_from_slice(&input_data);
- buf
- };
- let framed = frame_message(&input_payload);
- stream.write_all(&framed).expect("send input");
- stream.flush().expect("flush");
+ assert!(error.is_none(), "{error:?}");
+ wait_for_client_shell_bootstrap(&mut stream, Duration::from_secs(10))
+ .expect("initial client shell bootstrap");
- // Read subsequent frames — the server should have re-rendered after
- // the input was processed.
- let received_frame = wait_for_message_variant(&mut stream, Duration::from_secs(2), 1)
- .expect("wait for post-output frame");
- assert!(received_frame, "should receive a Frame after pane output");
+ let created = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "create-output-workspace",
+ "method": "workspace.create",
+ "params": {"label": "output", "focus": true}
+ })
+ .to_string(),
+ );
+ let pane_id = created["result"]["root_pane"]["pane_id"]
+ .as_str()
+ .expect("root pane id");
+ assert!(wait_for_message_variant(
+ &mut stream,
+ Duration::from_secs(5),
+ SERVER_MESSAGE_PANE_SURFACE,
+ )
+ .expect("wait for created workspace surface"));
+
+ let sent = send_json_request(
+ &api_socket,
+ &serde_json::json!({
+ "id": "send-output",
+ "method": "pane.send_text",
+ "params": {"pane_id": pane_id, "text": "printf 'test-output\\n'\\n"}
+ })
+ .to_string(),
+ );
+ assert!(sent.get("error").is_none(), "{sent}");
+ assert!(
+ wait_for_message_variants(
+ &mut stream,
+ Duration::from_secs(5),
+ &[
+ SERVER_MESSAGE_PANE_SURFACE,
+ SERVER_MESSAGE_PANE_SURFACE_PATCH,
+ ],
+ )
+ .expect("wait for post-output pane surface"),
+ "should receive a pane surface update after pane output"
+ );
cleanup_spawned_herdr(spawned, base);
}
@@ -1424,7 +1403,25 @@ fn pane_spawn_cwd_fallback_in_server() {
"fallback cwd should exist: {cwd}"
);
- cleanup_spawned_herdr(spawned, base);
+ let client_shell = spawn_client_shell_process(&config_home, &runtime_dir, &api_socket);
+ let output = spawn_pty_drain(
+ client_shell
+ ._master
+ .as_ref()
+ .expect("restored client shell PTY")
+ .try_clone_reader()
+ .expect("clone restored client shell reader"),
+ );
+ assert!(
+ wait_until(Duration::from_secs(8), Duration::from_millis(20), || {
+ read_output(&output).contains("missing-cwd")
+ }),
+ "client shell should render the restored session; output: {:?}",
+ read_output(&output)
+ );
+
+ drop(spawned);
+ cleanup_spawned_herdr(client_shell, base);
}
#[test]
@@ -1442,18 +1439,13 @@ fn graceful_shutdown_sends_server_shutdown_to_client() {
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_socket(&client_socket, Duration::from_secs(10));
- // Connect and handshake.
let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket");
- let (version, error) =
- client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed");
+ let (version, error) = client_shell_handshake(&mut stream, CURRENT_PROTOCOL, 54, 23)
+ .expect("handshake should succeed");
assert_eq!(version, CURRENT_PROTOCOL);
- assert!(error.is_none(), "{:?}", error);
-
- // Drain initial frame(s).
- stream
- .set_read_timeout(Some(Duration::from_secs(2)))
- .unwrap();
- while read_server_message(&mut stream).is_ok() {}
+ assert!(error.is_none(), "{error:?}");
+ wait_for_client_shell_bootstrap(&mut stream, Duration::from_secs(5))
+ .expect("client shell bootstrap");
// Send SIGINT to the server process to trigger graceful shutdown.
if let Some(pid) = spawned.child.process_id() {
@@ -1462,7 +1454,7 @@ fn graceful_shutdown_sends_server_shutdown_to_client() {
}
}
- // The client should receive a ServerShutdown message (variant 4)
+ // The client should receive a ServerShutdown message
// before the connection is closed, not just an abrupt EOF.
stream
.set_read_timeout(Some(Duration::from_secs(5)))
@@ -1471,8 +1463,8 @@ fn graceful_shutdown_sends_server_shutdown_to_client() {
match result {
Ok((variant, _payload)) => {
assert_eq!(
- variant, 4,
- "expected ServerShutdown (variant 4), got variant {variant}"
+ variant, SERVER_MESSAGE_SERVER_SHUTDOWN,
+ "expected ServerShutdown, got variant {variant}"
);
}
Err(e) => {
@@ -1501,9 +1493,9 @@ fn client_receives_notify_on_agent_state_change() {
let client_socket = runtime_dir.join("herdr-client.sock");
// Enable toast and sound in config so the server produces notifications.
- fs::create_dir_all(config_home.join("herdr")).unwrap();
+ fs::create_dir_all(config_home.join(app_dir_name())).unwrap();
fs::write(
- config_home.join("herdr/config.toml"),
+ config_home.join(app_dir_name()).join("config.toml"),
"onboarding = false\n[ui.toast]\nenabled = true\n[ui.sound]\nenabled = true\n",
)
.unwrap();
@@ -1541,18 +1533,13 @@ fn client_receives_notify_on_agent_state_change() {
wait_for_socket(&api_socket, Duration::from_secs(10));
wait_for_socket(&client_socket, Duration::from_secs(10));
- // Connect as a client and perform handshake.
let mut stream = UnixStream::connect(&client_socket).expect("should connect");
- let (version, error) =
- client_handshake(&mut stream, CURRENT_PROTOCOL, 80, 24).expect("handshake should succeed");
+ let (version, error) = client_shell_handshake(&mut stream, CURRENT_PROTOCOL, 54, 23)
+ .expect("handshake should succeed");
assert_eq!(version, CURRENT_PROTOCOL);
- assert!(error.is_none(), "{:?}", error);
-
- // Drain initial frame(s).
- stream
- .set_read_timeout(Some(Duration::from_secs(2)))
- .unwrap();
- while read_server_message(&mut stream).is_ok() {}
+ assert!(error.is_none(), "{error:?}");
+ wait_for_client_shell_bootstrap(&mut stream, Duration::from_secs(5))
+ .expect("client shell bootstrap");
// Create a workspace via the API.
let mut ws_stream = UnixStream::connect(&api_socket).expect("connect to API");
@@ -1596,8 +1583,7 @@ fn client_receives_notify_on_agent_state_change() {
let mut report_response = String::new();
report_reader.read_line(&mut report_response).unwrap();
- // Read messages from the client stream and look for Notify (variant 5).
- // Notify = ServerMessage variant index 5.
+ // Read messages from the client stream and look for the semantic notification.
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
@@ -1606,12 +1592,11 @@ fn client_receives_notify_on_agent_state_change() {
while Instant::now() < deadline {
match read_server_message(&mut stream) {
Ok((variant, _payload)) => {
- if variant == 5 {
- // ServerMessage::Notify — found it!
+ if variant == SERVER_MESSAGE_SEMANTIC_NOTIFICATION {
found_notify = true;
break;
}
- // Continue reading — Frame messages (variant 1) will come first.
+ // Snapshot and pane-surface messages may arrive first.
}
Err(_) => {
break;
@@ -1621,7 +1606,7 @@ fn client_receives_notify_on_agent_state_change() {
assert!(
found_notify,
- "client should receive a ServerMessage::Notify after pane.report_agent"
+ "client should receive a semantic notification after pane.report_agent"
);
// Now report Idle from Working — this should trigger a Done sound
@@ -1683,7 +1668,7 @@ fn client_receives_notify_on_agent_state_change() {
let mut idle_response = String::new();
idle_reader.read_line(&mut idle_response).unwrap();
- // Read messages and look for Done sound notify.
+ // Read messages and look for the done semantic notification.
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
@@ -1692,16 +1677,14 @@ fn client_receives_notify_on_agent_state_change() {
while Instant::now() < deadline {
match read_server_message(&mut stream) {
Ok((variant, _payload)) => {
- if variant == 5 {
- // Found a Notify message — that's good enough.
- // The test already verified the Blocked→Notify path above.
+ if variant == SERVER_MESSAGE_SEMANTIC_NOTIFICATION {
found_done_notify = true;
break;
}
- // Continue reading — Frame messages will come first.
+ // Snapshot and pane-surface messages may arrive first.
}
Err(e) => {
- eprintln!("read error while looking for Done Notify: {e}");
+ eprintln!("read error while looking for done notification: {e}");
break;
}
}
@@ -1709,7 +1692,7 @@ fn client_receives_notify_on_agent_state_change() {
assert!(
found_done_notify,
- "client should receive a Sound Notify with 'agent done' when background pane transitions Working→Idle"
+ "client should receive a semantic notification when a background pane transitions Working→Idle"
);
cleanup_spawned_herdr(spawned, base);