diff options
| author | Eric Yue <jiacheng.yue@foxmail.com> | 2026-09-15 21:28:59 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-15 15:28:59 +0200 |
| commit | 35707684b9de1b2e2b6be5de5a49e69ad57ac36b (patch) | |
| tree | 0f49470bf333e9d6a8874462a52eaf1961ff34f6 | |
| parent | f236ba54193d0f474b98849400b0d3a77d2056bd (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.rs | 47 | ||||
| -rw-r--r-- | tests/live_handoff.rs | 25 |
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!({ |
