test(terminal): keep typing at a pane whose shell is not reading yet

`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
This commit is contained in:
l0ng-ai
2026-09-09 00:01:07 +08:00
parent 9ef3b90651
commit ce1db2d867
+45 -4
View File
@@ -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);