Compare commits

...

1 Commits

Author SHA1 Message Date
Josh McKinney
bd74ac0247 test(tui): stabilize paste-burst tests
Bazel (experimental) linux-arm64 runs were intermittently failing in paste-burst tests because the tests depended on wall-clock timing.

Drive the fast-burst tests with a synthetic timeline and flush at a known timestamp so they stay deterministic across runner speeds.
2026-01-13 17:40:27 -08:00
2 changed files with 64 additions and 30 deletions

View File

@@ -2884,6 +2884,7 @@ mod tests {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use std::time::Duration;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
@@ -2898,13 +2899,12 @@ mod tests {
composer.set_steer_enabled(true);
composer.set_steer_enabled(true);
// Force an active burst so this test doesn't depend on tight timing.
composer
.paste_burst
.begin_with_retro_grabbed(String::new(), Instant::now());
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE));
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
// Use a synthetic timeline rather than relying on the wall clock to keep
// "fast burst" tests deterministic under slow runners (e.g., Bazel on arm64).
let mut now = Instant::now();
for ch in ['h', 'i'] {
feed_fast_ascii_burst_char(&mut composer, ch, &mut now);
}
let (result, _) =
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
@@ -2914,10 +2914,12 @@ mod tests {
);
for ch in ['t', 'h', 'e', 'r', 'e'] {
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
feed_fast_ascii_burst_char(&mut composer, ch, &mut now);
}
let _ = flush_after_paste_burst(&mut composer);
assert!(composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay() + Duration::from_millis(1)
));
assert_eq!(composer.textarea.text(), "hi\nthere");
}
@@ -3245,6 +3247,21 @@ mod tests {
composer.flush_paste_burst_if_due()
}
fn feed_fast_ascii_burst_char(composer: &mut ChatComposer, ch: char, now: &mut Instant) {
use std::time::Duration;
*now += Duration::from_millis(1);
match composer.paste_burst.on_plain_char(ch, *now) {
CharDecision::BufferAppend | CharDecision::BeginBufferFromPending => {
composer.paste_burst.append_char_to_buffer(ch, *now);
}
CharDecision::RetainFirstChar => {}
CharDecision::BeginBuffer { .. } => {
unreachable!("the fast-burst tests feed chars into an empty textarea")
}
}
}
// Test helper: simulate human typing with a brief delay and flush the paste-burst buffer
fn type_chars_humanlike(composer: &mut ChatComposer, chars: &[char]) {
use crossterm::event::KeyCode;

View File

@@ -2861,6 +2861,7 @@ mod tests {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use std::time::Duration;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
@@ -2873,13 +2874,12 @@ mod tests {
);
composer.set_steer_enabled(true);
// Force an active burst so this test doesn't depend on tight timing.
composer
.paste_burst
.begin_with_retro_grabbed(String::new(), Instant::now());
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE));
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
// Use a synthetic timeline rather than relying on the wall clock to keep
// "fast burst" tests deterministic under slow runners (e.g., Bazel on arm64).
let mut now = Instant::now();
for ch in ['h', 'i'] {
feed_fast_ascii_burst_char(&mut composer, ch, &mut now);
}
let (result, _) =
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
@@ -2889,10 +2889,12 @@ mod tests {
);
for ch in ['t', 'h', 'e', 'r', 'e'] {
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
feed_fast_ascii_burst_char(&mut composer, ch, &mut now);
}
let _ = flush_after_paste_burst(&mut composer);
assert!(composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay() + Duration::from_millis(1)
));
assert_eq!(composer.textarea.text(), "hi\nthere");
}
@@ -3217,6 +3219,21 @@ mod tests {
composer.flush_paste_burst_if_due()
}
fn feed_fast_ascii_burst_char(composer: &mut ChatComposer, ch: char, now: &mut Instant) {
use std::time::Duration;
*now += Duration::from_millis(1);
match composer.paste_burst.on_plain_char(ch, *now) {
CharDecision::BufferAppend | CharDecision::BeginBufferFromPending => {
composer.paste_burst.append_char_to_buffer(ch, *now);
}
CharDecision::RetainFirstChar => {}
CharDecision::BeginBuffer { .. } => {
unreachable!("the fast-burst tests feed chars into an empty textarea")
}
}
}
#[test]
fn slash_init_dispatches_command_and_does_not_submit_literal_text() {
use crossterm::event::KeyCode;
@@ -4432,9 +4449,7 @@ mod tests {
/// the payload is small, it should insert directly (no placeholder).
#[test]
fn burst_paste_fast_small_buffers_and_flushes_on_stop() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use std::time::Duration;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
@@ -4446,10 +4461,10 @@ mod tests {
false,
);
let mut now = Instant::now();
let count = 32;
for _ in 0..count {
let _ =
composer.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
feed_fast_ascii_burst_char(&mut composer, 'a', &mut now);
assert!(
composer.is_in_paste_burst(),
"expected active paste burst during fast typing"
@@ -4464,7 +4479,9 @@ mod tests {
composer.textarea.text().is_empty(),
"text should remain empty until flush"
);
let flushed = flush_after_paste_burst(&mut composer);
let flushed = composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay() + Duration::from_millis(1),
);
assert!(flushed, "expected buffered text to flush after stop");
assert_eq!(composer.textarea.text(), "a".repeat(count));
assert!(
@@ -4477,9 +4494,7 @@ mod tests {
/// the payload is large, it should insert a placeholder and defer the full text until submit.
#[test]
fn burst_paste_fast_large_inserts_placeholder_on_flush() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use std::time::Duration;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
@@ -4491,15 +4506,17 @@ mod tests {
false,
);
let mut now = Instant::now();
let count = LARGE_PASTE_CHAR_THRESHOLD + 1; // > threshold to trigger placeholder
for _ in 0..count {
let _ =
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
feed_fast_ascii_burst_char(&mut composer, 'x', &mut now);
}
// Nothing should appear until we stop and flush
assert!(composer.textarea.text().is_empty());
let flushed = flush_after_paste_burst(&mut composer);
let flushed = composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay() + Duration::from_millis(1),
);
assert!(flushed, "expected flush after stopping fast input");
let expected_placeholder = format!("[Pasted Content {count} chars]");