test(terminal): require the runtime to pump deferred work

Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>

Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
This commit is contained in:
npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf
2026-08-02 01:53:12 -04:00
parent 3d4fc49df9
commit 5f416f7d25
4 changed files with 36 additions and 8 deletions

View File

@@ -130,8 +130,8 @@ pub fn max_atom_work(columns: usize, lines: usize, scrollback: usize) -> u64 {
/// fixtures caught it:
///
/// * The budget is checked *between* slices, so a drain overshoots by up to
/// one whole slice -- not one atom. [`slice_bytes`] keeps that under one
/// budget wherever its derivation is unclamped.
/// one whole slice -- not one atom. [`slice_bytes_remaining`] keeps that
/// under one budget wherever its derivation is unclamped.
/// * A callback already running cannot be preempted. RIS at the default 10k
/// scrollback is worth 16x the whole budget on its own, so on such a grid
/// the floor binds and the overshoot is a few of those atoms. No scheduler

View File

@@ -161,6 +161,11 @@ impl Terminal {
}
/// Whether the tail is at its cap and the reader must stop reading.
///
/// There is no production consumer today, deliberately: the desktop
/// runtime pumps `drain()` to completion after every read, so the tail is
/// empty between iterations. A future reader that defers pumping must
/// consult this signal before accepting more PTY bytes.
pub fn tail_full(&self) -> bool {
self.feeder.tail_full()
}

View File

@@ -172,8 +172,9 @@ impl Feeder {
/// per count when it finds no stop.
///
/// What *is* bounded is the number of atoms per slice, and that bound
/// holds from the first byte of a cold feeder: [`slice_bytes`] is derived
/// from the densest work-per-byte upstream can produce on this grid, so
/// holds from the first byte of a cold feeder:
/// [`slice_bytes_remaining`] is derived from the densest work-per-byte
/// upstream can produce on this grid, so
/// no slice can contain more than one budget's worth of callbacks no
/// matter what the payload is or what the feeder has seen before.
///

View File

@@ -203,6 +203,15 @@ fn wire_publication(publication: Publication) -> Result<FrameMessage> {
})
}
fn feed_and_drain(terminal: &SharedTerminal, bytes: &[u8]) -> bool {
let mut more = terminal.feed(bytes);
let deferred = more;
while more && !terminal.is_closing() {
more = terminal.drain();
}
deferred
}
struct ReaderThread {
handle: Option<JoinHandle<()>>,
terminal: Arc<SharedTerminal>,
@@ -502,10 +511,7 @@ pub(crate) fn terminal_attach(
if reader_terminal.is_closing() {
continue;
}
let mut more = reader_terminal.feed(&buffer[..count]);
while more && !reader_terminal.is_closing() {
more = reader_terminal.drain();
}
let _ = feed_and_drain(&reader_terminal, &buffer[..count]);
if reader_terminal.is_closing() {
continue;
}
@@ -815,6 +821,22 @@ mod tests {
assert!(successor.frame.full);
}
#[test]
fn reader_pumps_a_deferred_tail_without_an_external_event() {
let (terminal, _actions) = Terminal::new(Size::default(), Fences::ALL);
let terminal = SharedTerminal::new(terminal);
let payload = "\u{1b}c".repeat(2_102_714);
assert!(
feed_and_drain(&terminal, payload.as_bytes()),
"fixture must defer parser work before the runtime pumps it"
);
let terminal = terminal.lock();
assert_eq!(terminal.pending_bytes(), 0);
assert_eq!(terminal.stats().completed_units, 2_102_714);
}
#[test]
fn initial_attach_retains_output_captured_after_its_bootstrap_snapshot() {
let viewport = marker_frame(0, true).viewport;