aboutsummaryrefslogtreecommitdiff
path: root/tests/support/mod.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /tests/support/mod.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
Diffstat (limited to 'tests/support/mod.rs')
-rw-r--r--tests/support/mod.rs152
1 files changed, 116 insertions, 36 deletions
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
index f295dbbe..1da9985b 100644
--- a/tests/support/mod.rs
+++ b/tests/support/mod.rs
@@ -1,5 +1,3 @@
-#![allow(dead_code)]
-
use std::collections::HashSet;
use std::fs;
use std::io::{Read, Write};
@@ -16,6 +14,13 @@ static CLEANUP_GUARD: OnceLock<CleanupGuard> = OnceLock::new();
const WATCHDOG_SCAN_INTERVAL: Duration = Duration::from_secs(1);
const RUNTIME_OWNER_MARKER: &str = ".herdr-test-owner-pid";
pub const CURRENT_PROTOCOL: u32 = 21;
+pub const SERVER_MESSAGE_SERVER_SHUTDOWN: u32 = 3;
+pub const SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT: u32 = 12;
+pub const SERVER_MESSAGE_PANE_SURFACE: u32 = 13;
+pub const SERVER_MESSAGE_SEMANTIC_NOTIFICATION: u32 = 14;
+pub const SERVER_MESSAGE_PANE_SURFACE_PATCH: u32 = 19;
+const CLIENT_MESSAGE_CLIENT_SHELL_HELLO: u32 = 11;
+const CLIENT_MESSAGE_CLIENT_SHELL_PANE_INPUT: u32 = 13;
pub fn register_spawned_herdr_pid(pid: Option<u32>) {
let Some(pid) = pid else {
@@ -108,7 +113,7 @@ pub fn wait_for_file(path: &Path, timeout: Duration) {
panic!("file did not appear at {}", path.display());
}
-pub fn encode_varint_u32(v: u32) -> Vec<u8> {
+fn encode_varint_u32(v: u32) -> Vec<u8> {
if v < 251 {
vec![v as u8]
} else if v < 65536 {
@@ -122,7 +127,7 @@ pub fn encode_varint_u32(v: u32) -> Vec<u8> {
}
}
-pub fn encode_varint_u16(v: u16) -> Vec<u8> {
+fn encode_varint_u16(v: u16) -> Vec<u8> {
if v < 251 {
vec![v as u8]
} else {
@@ -132,14 +137,14 @@ pub fn encode_varint_u16(v: u16) -> Vec<u8> {
}
}
-pub fn frame_message(payload: &[u8]) -> Vec<u8> {
+fn frame_message(payload: &[u8]) -> Vec<u8> {
let len = payload.len() as u32;
let mut framed = len.to_le_bytes().to_vec();
framed.extend_from_slice(payload);
framed
}
-pub fn decode_varint_u32(payload: &[u8], offset: usize) -> Result<(u32, usize), String> {
+fn decode_varint_u32(payload: &[u8], offset: usize) -> Result<(u32, usize), String> {
if offset >= payload.len() {
return Err("payload too short for varint".into());
}
@@ -220,16 +225,35 @@ fn decode_welcome(payload: &[u8]) -> Result<(u32, Option<String>), String> {
Ok((version, error))
}
-pub fn client_handshake(
+fn finish_handshake(
stream: &mut UnixStream,
- version: u32,
- cols: u16,
- rows: u16,
+ hello_payload: &[u8],
) -> Result<(u32, Option<String>), String> {
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.map_err(|e| e.to_string())?;
+ stream
+ .write_all(&frame_message(hello_payload))
+ .map_err(|e| e.to_string())?;
+ stream.flush().map_err(|e| e.to_string())?;
+
+ let mut len_buf = [0u8; 4];
+ stream.read_exact(&mut len_buf).map_err(|e| e.to_string())?;
+ let len = u32::from_le_bytes(len_buf) as usize;
+ if len > 2 * 1024 * 1024 {
+ return Err(format!("oversized response: {len}"));
+ }
+ let mut payload = vec![0u8; len];
+ stream.read_exact(&mut payload).map_err(|e| e.to_string())?;
+ decode_welcome(&payload)
+}
+pub fn client_handshake(
+ stream: &mut UnixStream,
+ version: u32,
+ cols: u16,
+ rows: u16,
+) -> Result<(u32, Option<String>), String> {
let hello_payload = encode_varint_enum(
0,
&[
@@ -238,25 +262,33 @@ pub fn client_handshake(
&encode_varint_u16(rows),
&encode_varint_u32(8), // cell_width_px
&encode_varint_u32(16), // cell_height_px
- &encode_varint_u32(0), // RenderEncoding::SemanticFrame
- &encode_varint_u32(0), // ClientKeybindings::Server
- &encode_varint_u32(0), // ClientLaunchMode::App
+ &[0], // pixel_mouse = false
],
);
- let framed = frame_message(&hello_payload);
- stream.write_all(&framed).map_err(|e| e.to_string())?;
- stream.flush().map_err(|e| e.to_string())?;
-
- let mut len_buf = [0u8; 4];
- stream.read_exact(&mut len_buf).map_err(|e| e.to_string())?;
- let len = u32::from_le_bytes(len_buf) as usize;
- if len > 2 * 1024 * 1024 {
- return Err(format!("oversized response: {len}"));
- }
+ finish_handshake(stream, &hello_payload)
+}
- let mut payload = vec![0u8; len];
- stream.read_exact(&mut payload).map_err(|e| e.to_string())?;
- decode_welcome(&payload)
+pub fn client_shell_handshake(
+ stream: &mut UnixStream,
+ version: u32,
+ surface_cols: u16,
+ surface_rows: u16,
+) -> Result<(u32, Option<String>), String> {
+ let hello_payload = encode_varint_enum(
+ CLIENT_MESSAGE_CLIENT_SHELL_HELLO,
+ &[
+ &encode_varint_u32(version),
+ &encode_varint_u32(8),
+ &encode_varint_u32(16),
+ &encode_varint_u16(surface_cols),
+ &encode_varint_u16(surface_rows),
+ &[0], // pixel mouse disabled
+ &[0], // direct graphics disabled
+ &[0], // client-owned keybindings
+ &[0], // mouse capture disabled
+ ],
+ );
+ finish_handshake(stream, &hello_payload)
}
pub fn read_server_message(stream: &mut UnixStream) -> Result<(u32, Vec<u8>), String> {
@@ -281,16 +313,27 @@ pub fn read_server_message(stream: &mut UnixStream) -> Result<(u32, Vec<u8>), St
Ok((variant, payload[consumed..].to_vec()))
}
-pub fn send_input(stream: &mut UnixStream, data: &[u8]) -> Result<(), String> {
- let mut buf = encode_varint_u32(1);
- buf.extend_from_slice(&encode_varint_u32(data.len() as u32));
- buf.extend_from_slice(data);
- let framed = frame_message(&buf);
+pub fn send_client_shell_shift_enter(stream: &mut UnixStream, pane_id: &str) -> Result<(), String> {
+ let mut payload = encode_varint_u32(CLIENT_MESSAGE_CLIENT_SHELL_PANE_INPUT);
+ payload.extend_from_slice(&encode_varint_u32(pane_id.len() as u32));
+ payload.extend_from_slice(pane_id.as_bytes());
+ payload.extend_from_slice(&encode_varint_u32(1)); // one pane input event
+ payload.extend_from_slice(&encode_varint_u32(0)); // Key
+ payload.extend_from_slice(&encode_varint_u32(1)); // Enter
+ payload.push(1); // Shift
+ payload.extend_from_slice(&encode_varint_u32(0)); // Press
+ payload.extend_from_slice(&encode_varint_u16(1));
+ payload.push(0); // no shifted codepoint
+ payload.push(0); // no generated text
+ payload.push(0); // does not track release
+ payload.push(0); // no physical key id
+
stream
- .write_all(&framed)
- .map_err(|e| format!("write input: {e}"))?;
- stream.flush().map_err(|e| format!("flush input: {e}"))?;
- Ok(())
+ .write_all(&frame_message(&payload))
+ .map_err(|e| format!("write client shell key: {e}"))?;
+ stream
+ .flush()
+ .map_err(|e| format!("flush client shell key: {e}"))
}
pub fn send_detach(stream: &mut UnixStream) -> Result<(), String> {
@@ -330,13 +373,21 @@ pub fn wait_for_message_variant(
timeout: Duration,
variant: u32,
) -> Result<bool, String> {
+ wait_for_message_variants(stream, timeout, &[variant])
+}
+
+pub fn wait_for_message_variants(
+ stream: &mut UnixStream,
+ timeout: Duration,
+ variants: &[u32],
+) -> Result<bool, 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((got, _)) if got == variant => return Ok(true),
+ Ok((got, _)) if variants.contains(&got) => return Ok(true),
Ok(_) => continue,
Err(_) => continue,
}
@@ -344,6 +395,35 @@ pub fn wait_for_message_variant(
Ok(false)
}
+pub fn wait_for_client_shell_bootstrap(
+ stream: &mut UnixStream,
+ timeout: Duration,
+) -> Result<(), String> {
+ stream
+ .set_read_timeout(Some(Duration::from_millis(200)))
+ .map_err(|e| e.to_string())?;
+ let deadline = Instant::now() + timeout;
+ let mut saw_snapshot = false;
+ while Instant::now() < deadline {
+ match read_server_message(stream) {
+ Ok((SERVER_MESSAGE_CLIENT_SHELL_SNAPSHOT, _)) => saw_snapshot = true,
+ Ok((SERVER_MESSAGE_PANE_SURFACE, _)) if saw_snapshot => return Ok(()),
+ Ok((SERVER_MESSAGE_PANE_SURFACE, _)) => {
+ return Err("client shell pane surface arrived before its snapshot".into());
+ }
+ Ok(_) | Err(_) => {}
+ }
+ }
+ Err(format!(
+ "timed out waiting for client shell {}",
+ if saw_snapshot {
+ "pane surface"
+ } else {
+ "snapshot"
+ }
+ ))
+}
+
pub fn wait_for_disconnect(stream: &mut UnixStream, timeout: Duration) -> Result<bool, String> {
stream.set_nonblocking(true).map_err(|e| e.to_string())?;
let deadline = Instant::now() + timeout;