From ce1db2d8675cb5b6579746f34f65c5caeee8e17b Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:01:07 +0800 Subject: [PATCH] test(terminal): keep typing at a pane whose shell is not reading yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `a_pane_re_attached_after_a_switch_gets_its_screen_back` and `the_later_of_two_racing_attaches_keeps_the_screen_and_the_seat` both failed on the Windows runner with a grid that was still completely empty after 15s — not even a prompt had been printed, so the first command was typed at a shell that had not started reading. `DaemonPane::spawn` returning means the pty exists, not that the shell behind it is up: on a loaded runner it can be seconds behind, and on Windows the ConPTY has not necessarily connected the child to its input pipe at all, so bytes written in that window reach nobody and nothing ever echoes. On unix the same bytes simply wait in the pty buffer, which is why this only ever showed on Windows. The first command of each test now goes through `type_until_echoed`, which retypes it every 2s for up to 30s. Retyping is safe for everything these tests assert: a line that did land and was merely slow runs twice, and every assertion is a `contains`. The later commands keep their single `write_input` — by then the shell has echoed once, which is proof it is reading. Verified on macOS: all six replay cases pass in 0.75s, so the retry costs nothing when the first write lands. The Windows failure is intermittent and could not be reproduced locally. Claude-Session: https://claude.ai/code/session_01Mnerr8RZ23Nd4cxyfeqxiu --- src/terminal/remote.rs | 49 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/terminal/remote.rs b/src/terminal/remote.rs index 29ab35be..b2f67038 100644 --- a/src/terminal/remote.rs +++ b/src/terminal/remote.rs @@ -3261,6 +3261,40 @@ mod replay_tests { false } + /// Types `line` at the pane until the grid echoes `needle` back, and + /// answers whether it ever did. + /// + /// One write is not enough for the *first* command a fresh pane is given. + /// `spawn` returning means the pty exists, not that the shell behind it + /// has started, printed a prompt, or begun reading — and on Windows the + /// ConPTY has not necessarily connected the child to its input pipe yet, + /// so bytes typed into that window reach nobody at all. On a loaded runner + /// the window is wide: both cases showed up on Windows CI as a grid that + /// was still completely empty after 15s, so not even a prompt had been + /// printed, let alone an echo. + /// + /// Retyping is safe for everything asserted here: a line that did land and + /// was merely slow simply runs twice, and every assertion is a `contains`. + /// The later commands in these tests keep their single `write_input` — + /// by then the shell has echoed once, which is proof it is reading. + fn type_until_echoed( + pane: &tty7_core::daemon::pane::DaemonPane, + line: &[u8], + term: &RemoteTerminal, + needle: &str, + ) -> bool { + for _ in 0..15 { + pane.write_input(line); + for _ in 0..80 { + if all_text(term).contains(needle) { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(25)); + } + } + false + } + /// Switching workspaces drops every pane and attaches to the same daemon /// panes again. Whatever the pane put on screen while the window was /// elsewhere — and whatever it had already — has to come back with it. @@ -3286,9 +3320,13 @@ mod replay_tests { // pane is not 80x24 on screen, so the real geometry goes down the link // and the grid waits for the daemon to echo it back. first.resize(TermSize::new(120, 40), 8, 17); - pane.write_input(b"echo BEFORE-THE-SWITCH\r"); assert!( - wait_for(&first, "BEFORE-THE-SWITCH"), + type_until_echoed( + &pane, + b"echo BEFORE-THE-SWITCH\r", + &first, + "BEFORE-THE-SWITCH" + ), "the pane never echoed the first command; grid held:\n{}", all_text(&first) ); @@ -3367,8 +3405,11 @@ mod replay_tests { let (first_epoch, mut first, first_forward) = attach_client(&pane); first.resize(TermSize::new(120, 40), 8, 17); - pane.write_input(b"echo RACED-OUTPUT\r"); - assert!(wait_for(&first, "RACED-OUTPUT"), "the pane never echoed"); + assert!( + type_until_echoed(&pane, b"echo RACED-OUTPUT\r", &first, "RACED-OUTPUT"), + "the pane never echoed; grid held:\n{}", + all_text(&first) + ); // The second rebuild attaches before the first one's view is dropped. let (second_epoch, second, second_forward) = attach_client(&pane);