aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Yue <jiacheng.yue@foxmail.com>2026-09-15 21:28:59 +0800
committerGitHub <noreply@github.com>2026-09-15 15:28:59 +0200
commit35707684b9de1b2e2b6be5de5a49e69ad57ac36b (patch)
tree0f49470bf333e9d6a8874462a52eaf1961ff34f6
parentf236ba54193d0f474b98849400b0d3a77d2056bd (diff)
test: wait for runtime readiness in integration tests (#4168)
Root cause: The handoff test starts an agent immediately after workspace creation, before the new shell is necessarily ready. The reconnect test assumes a fixed sidebar row and searches raw ANSI output for text that can arrive as separate incremental cell updates. Fix: Wait for a shell builtin to write a readiness marker before sending the single raw agent.start request. Exercise the gap with a delayed shell. Locate the recovered workspace in the rendered sidebar and check the reconstructed screen for selection and input, including overlay borders. Do not extend existing timeouts or add retries that hide failures. Validation: All 25 affected group and helper tests pass. Ten repeated rounds of the two runtime tests and the row helper pass, for 30 test executions without retries. Standalone native just ci passes 3582 Rust tests with six default skips, formatting, all-target Clippy, 106 maintenance tests, six architecture tests and 39 integration-asset tests. Windows cross-lint was not run because its SDK is not installed. Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
-rw-r--r--tests/client_mode.rs47
-rw-r--r--tests/live_handoff.rs25
2 files changed, 65 insertions, 7 deletions
diff --git a/tests/client_mode.rs b/tests/client_mode.rs
index a946af7c..71d3ff8d 100644
--- a/tests/client_mode.rs
+++ b/tests/client_mode.rs
@@ -716,6 +716,39 @@ fn output_len(output: &SharedOutput) -> usize {
output.lock().unwrap_or_else(|p| p.into_inner()).text.len()
}
+fn sidebar_row_click(screen: &str, label: &str) -> Vec<u8> {
+ let sidebar_width = screen
+ .lines()
+ .find_map(|line| line.chars().position(|character| character == '│'))
+ .expect("visible sidebar boundary");
+ let row = screen
+ .lines()
+ .position(|line| {
+ line.chars()
+ .take(sidebar_width)
+ .collect::<String>()
+ .contains(label)
+ })
+ .unwrap_or_else(|| panic!("sidebar row {label:?} is not visible: {screen}"))
+ + 1;
+ format!("\x1b[<0;7;{row}M\x1b[<0;7;{row}m").into_bytes()
+}
+
+#[test]
+fn sidebar_row_click_tracks_restored_workspace_count() {
+ for restored in [false, true] {
+ let screen = format!(
+ " machines │\n │\n ▾ Local │local-returned in pane output\n{} · local-returned └─────────────────\n",
+ if restored { " · restored │\n" } else { "" }
+ );
+ let row = if restored { 5 } else { 4 };
+ assert_eq!(
+ sidebar_row_click(&screen, "local-returned"),
+ format!("\x1b[<0;7;{row}M\x1b[<0;7;{row}m").into_bytes()
+ );
+ }
+}
+
/// Spawns a server + real thin client under a PTY and waits until the client
/// has attached and rendered a frame. Returns the pieces plus a shared buffer
/// that keeps accumulating PTY output (including teardown) on a background
@@ -1074,20 +1107,22 @@ fn federated_client_starts_without_local_and_survives_its_restart() {
local_pane,
"printf 'LOCAL_RECOVERED_SURFACE\\n'",
);
- let watermark = output_len(&output);
// Select the fresh workspace below Local's restored workspace.
- input.write_all(b"\x1b[<0;7;5M\x1b[<0;7;5m").unwrap();
+ // A fast shutdown may leave no saved workspace, so locate the actual row.
+ input
+ .write_all(&sidebar_row_click(&screen_text(), "local-returned"))
+ .unwrap();
assert!(
wait_until(Duration::from_secs(10), Duration::from_millis(20), || {
- read_output(&output)[watermark..].contains("LOCAL_RECOVERED_SURFACE")
+ screen_text().contains("LOCAL_RECOVERED_SURFACE")
}),
"recovered Local must be selectable: {}",
- read_output(&output)
+ screen_text()
);
// A coherent frame precedes the final host-effects fence; input stays gated until then.
assert!(
wait_until(Duration::from_secs(8), Duration::from_millis(100), || {
- if read_output(&output)[watermark..].contains("LOCAL_INPUT_RECOVERED") {
+ if screen_text().contains("LOCAL_INPUT_RECOVERED") {
return true;
}
input
@@ -1096,7 +1131,7 @@ fn federated_client_starts_without_local_and_survives_its_restart() {
false
}),
"recovered Local must accept input: {}",
- read_output(&output)
+ screen_text()
);
drop(input);
drop(client);
diff --git a/tests/live_handoff.rs b/tests/live_handoff.rs
index e40f8268..ea4b84df 100644
--- a/tests/live_handoff.rs
+++ b/tests/live_handoff.rs
@@ -1581,9 +1581,13 @@ fn live_handoff_keeps_agent_started_pane_after_agent_exits() {
let api_socket = runtime_dir.join("herdr.sock");
let started_marker = base.join("agent-started");
let exited_marker = base.join("agent-exited");
+ let ready_marker = base.join("shell-ready");
let shell_marker = base.join("shell-after-agent");
let bin = base.join("bin");
fs::create_dir_all(&bin).unwrap();
+ let delayed_shell = bin.join("delayed-shell");
+ fs::write(&delayed_shell, "#!/bin/sh\n/bin/sleep 0.4\nexec /bin/sh\n").unwrap();
+ fs::set_permissions(&delayed_shell, fs::Permissions::from_mode(0o755)).unwrap();
let fake_pi = bin.join("pi");
fs::write(
&fake_pi,
@@ -1601,7 +1605,10 @@ fn live_handoff_keeps_agent_started_pane_after_agent_exits() {
&config_home,
&runtime_dir,
&api_socket,
- &[("PATH", path.as_str())],
+ &[
+ ("PATH", path.as_str()),
+ ("SHELL", delayed_shell.to_str().unwrap()),
+ ],
);
wait_for_socket(&api_socket, Duration::from_secs(10));
register_runtime_dir(&runtime_dir);
@@ -1619,6 +1626,22 @@ fn live_handoff_keeps_agent_started_pane_after_agent_exits() {
.unwrap()
.to_string();
+ assert_ok(request(
+ &api_socket,
+ serde_json::json!({
+ "id": "test:shell-ready",
+ "method": "pane.send_input",
+ "params": {
+ "pane_id": pane_id,
+ "text": format!("printf ready > {}", ready_marker.display()),
+ "keys": ["Enter"]
+ }
+ }),
+ ));
+ // Creation acknowledges the PTY, not an idle interactive shell. A real
+ // shell command must execute before this raw agent.start request.
+ support::wait_for_file(&ready_marker, Duration::from_secs(5));
+
let started = request(
&api_socket,
serde_json::json!({