newt-tui 0.8.0

Newt-Agent TUI surfaces (ratatui): code mode + pilot mode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
//! **Real-PTY acceptance test for the operator panels' RAW-MODE lifecycle**
//! (#1889).
//!
//! `config_panel::run` and `backend_panel::run` take a real terminal into raw
//! mode and must hand it back on **every** exit path. Both used to call
//! `disable_raw_mode()` as a statement after the loop closure: an error return
//! reached it, and a panic unwound straight past, leaving the operator in a
//! shell with no echo. [`PanelRawGuard`] makes the restore a Drop obligation;
//! this is what says so against an actual tty.
//!
//! Modelled on `interaction_view_pty_test` (C2a, #1876) and, through it,
//! `transcript_pager_pty_test` (#1677) — including the control that makes the
//! result mean anything: **`raw_during`**. A test that never observed raw mode
//! would prove restoration vacuously; it would pass against a build where
//! `PanelRawGuard::enter` did nothing at all.
//!
//! # Why a child process is unavoidable
//!
//! A Drop-on-unwind restoration cannot be observed from inside the process
//! doing the unwinding. The parent must outlive the child and read the tty's
//! own state. Running it in-process would leave a test that owns no terminal,
//! which proves nothing.
//!
//! # The postcondition is KERNEL state, not escape bytes
//!
//! [`Pty::is_raw`] runs `tcgetattr` on the parent's own slave fd, and pty line
//! discipline belongs to the *device*, so the parent observes exactly the
//! raw/cooked state the child installed. A child could emit a plausible
//! restore sequence and still leave the tty raw — and that is precisely the
//! failure that wrecks a terminal.
//!
//! # What this does NOT prove, and what does
//!
//! It proves the GUARD restores. It cannot prove the panels USE the guard —
//! driving `run` needs an interactive key loop. A guard that is correct and
//! unused is exactly the state these files were in, so that half is pinned
//! structurally by `config_panel::tests::enter_panel_raw_mode_is_the_only_way_in`.
//! Neither test is sufficient alone.

use std::time::{Duration, Instant};

use tests_pty::{screen_grid, signal_winch, Pty};

use crate::panel::PanelRawGuard;
use crate::prompt_visibility_test::wait_for_child;

const CHILD_TEST: &str = "panel_raw_mode_pty_test::panel_raw_mode_child";

/// Generous: this tier runs under parallel load on shared runners.
const REACH_TIMEOUT: Duration = Duration::from_secs(60);
const EXIT_TIMEOUT: Duration = Duration::from_secs(60);

/// #1950 fixture: the panel's inline height, and what it draws.
const PANEL_TEST_HEIGHT: u16 = 6;
const PANEL_SENTINEL: &str = "PANEL-DREW-1950";
const PANEL_SENTINEL_OK: &str = "PANEL-OPENED-1950";

/// #1977 fixture. The BODY of the panel, distinct from its border: the
/// operator's report was a visible top border with no rows under it.
const PANEL_BODY_SENTINEL: &str = "PANEL-BODY-1977";
/// The competing bottom-pinned viewport, standing in for the rich prompt.
const PROMPT_SENTINEL: &str = "PROMPT-ROWS-1977";

/// #2573 hardening fixture: the rows a competing holder keeps, and what it
/// paints in them.
const HOLDER_ROWS: u16 = 3;
const HOLDER_SENTINEL: &str = "HOLDER-ROW";

/// A bordered panel, `Esc` to close: the smallest `Screen` that draws a
/// height the parent can count from its rounded corners.
struct HeldPanel;

impl crate::panel::Screen for HeldPanel {
    fn draw(&self, frame: &mut ratatui::Frame) {
        use ratatui::widgets::{Block, BorderType, Paragraph};
        let body = (0..30)
            .map(|n| format!("panel-row-{n:02}"))
            .collect::<Vec<_>>();
        frame.render_widget(
            Paragraph::new(body.join("\n")).block(
                Block::bordered()
                    .border_type(BorderType::Rounded)
                    .title("held"),
            ),
            frame.area(),
        );
    }

    fn key(&mut self, key: crate::panel::Key) -> crate::panel::Flow {
        match key {
            crate::panel::Key::Esc => crate::panel::Flow::Close(false),
            _ => crate::panel::Flow::Stay,
        }
    }
}

/// The child half: takes the real terminal exactly as a panel does.
/// `NEWT_PANEL_PTY_CHILD` selects the lifecycle.
#[test]
#[ignore = "child process of the panel raw-mode PTY acceptance test"]
fn panel_raw_mode_child() {
    let Some(mode) = std::env::var_os("NEWT_PANEL_PTY_CHILD") else {
        return;
    };
    // HOLD the guard until the parent has sampled the tty. Exiting immediately
    // is a race the parent loses: the restore would run before the first
    // `is_raw()` read and the test would "pass" without ever having observed
    // raw mode — which is what `raw_during` refuses to accept.
    let hold = || {
        use std::io::Read as _;
        let mut byte = [0u8; 1];
        let _ = std::io::stdin().read(&mut byte);
    };
    match mode.to_string_lossy().as_ref() {
        // The ordinary path: the panel closes and the scope ends.
        "clean" => {
            let _guard = PanelRawGuard::enter().expect("enter raw mode");
            hold();
        }
        // THE ONE THAT MATTERS. A panic with the panel up — the path the bare
        // `disable_raw_mode()` statement skipped entirely.
        "panic" => {
            let _guard = PanelRawGuard::enter().expect("enter raw mode");
            hold();
            panic!("synthetic failure while the panel is up (#1889)");
        }
        // An error RETURN out of the middle, which is how a failed
        // `terminal.draw` leaves `run`. The guard is dropped by scope unwind,
        // not by cleanup the author remembered to write.
        "error" => {
            fn inner() -> std::io::Result<()> {
                let _guard = PanelRawGuard::enter()?;
                {
                    use std::io::Read as _;
                    let mut byte = [0u8; 1];
                    let _ = std::io::stdin().read(&mut byte);
                }
                Err(std::io::Error::other("synthetic draw failure"))
            }
            assert!(inner().is_err(), "the fixture must take the error path");
        }
        // #1950: THE REPORTED FAILURE. A competing consumer of the same tty
        // while the panel opens. `Viewport::Inline` makes ratatui ask the
        // terminal where the cursor is (`ESC[6n`, answered on the INPUT
        // stream) — and the reader below takes the answer first, so the panel
        // used to fail to open at all with "The cursor position could not be
        // read within a normal duration".
        //
        // The parent DOES answer the query. That is the point: the terminal is
        // cooperative and the panel must still open, so a pass cannot be
        // explained by the query never having been asked.
        "inline_contended" => {
            let _guard = PanelRawGuard::enter().expect("enter raw mode");
            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
            let reader = {
                let stop = stop.clone();
                std::thread::spawn(move || {
                    while !stop.load(std::sync::atomic::Ordering::Relaxed) {
                        let _ = crossterm::event::poll(Duration::from_millis(50));
                        let _ = crossterm::event::read();
                    }
                })
            };
            // Let the reader reach its blocking read before the panel asks.
            std::thread::sleep(Duration::from_millis(150));

            match crate::panel::make_terminal(PANEL_TEST_HEIGHT) {
                Ok(mut terminal) => {
                    // RENDERS, not merely constructs. A terminal that was
                    // built and then drew nothing would leave the operator
                    // looking at the same blank screen the bug produced.
                    terminal
                        .draw(|f| {
                            f.render_widget(
                                ratatui::widgets::Paragraph::new(PANEL_SENTINEL),
                                f.area(),
                            );
                        })
                        .expect("the panel draws");
                    println!("{PANEL_SENTINEL_OK}");
                }
                Err(e) => println!("PANEL-FAILED {e}"),
            }
            use std::io::Write as _;
            std::io::stdout().flush().ok();
            stop.store(true, std::sync::atomic::Ordering::Relaxed);
            // Deliberately NOT `hold()`, and deliberately not joined. The
            // reader is blocked in `event::read`, so joining would wait for a
            // keystroke — and it would EAT the byte the parent sends to
            // release a hold, which is exactly how the first draft of this
            // fixture hung for 60s and reported the terminal left raw. The
            // parent has already sampled `is_raw` by now: the child took the
            // guard, slept 150ms, and spent a DSR round trip before drawing.
            // Falling off the end here drops the guard and restores the tty,
            // which is the property the rest of this file exists to hold.
            drop(reader);
        }
        // #1977: TWO bottom-anchored inline viewports, which is the operator's
        // live report. The competing reader eats the DSR reply, so BOTH take
        // the anchored fallback and land on the same rows — the environment
        // the defect was found in, made rather than mocked.
        "inline_overpainted" => {
            let _guard = PanelRawGuard::enter().expect("enter raw mode");
            let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
            let reader = {
                let stop = stop.clone();
                std::thread::spawn(move || {
                    while !stop.load(std::sync::atomic::Ordering::Relaxed) {
                        let _ = crossterm::event::poll(Duration::from_millis(50));
                        let _ = crossterm::event::read();
                    }
                })
            };
            std::thread::sleep(Duration::from_millis(150));

            // The PROMPT viewport, as `rich_input` owns it. Same height as
            // the panel and the same bottom anchor, so the two cover the SAME
            // rows — which is #1977's geometry. A shorter prompt overlaps only
            // part of the panel and the assertion below could pass on a row
            // that was never contested.
            let prompt_lease = crate::inline_viewport::lease_bottom_rows(
                PANEL_TEST_HEIGHT,
                newt_core::tty::OnCollision::Refuse,
            )
            .expect("the prompt takes the bottom rows first");
            let mut prompt = crate::inline_viewport::inline_terminal(prompt_lease)
                .expect("prompt viewport opens");
            // The content must CHANGE on each paint. ratatui diffs cells and
            // emits only what differs, so a prompt repainting identical text
            // writes nothing at all — the first cut of this fixture painted
            // six times and produced one write, contending with nothing.
            // EVERY row changes on every paint. ratatui diffs cells, so a
            // prompt repainting near-identical text rewrites only the columns
            // that differ — the first cut of this fixture painted six times
            // and emitted one changed digit, contending with nothing.
            let paint_prompt = |t: &mut crate::inline_viewport::InlineTerm, tick: usize| {
                let filled = (0..PANEL_TEST_HEIGHT)
                    .map(|r| format!("{PROMPT_SENTINEL} r{r} t{tick}"))
                    .collect::<Vec<_>>()
                    .join("\n");
                // `clear` before drawing, because ratatui diffs against its OWN
                // buffer and this viewport's belief about the screen is stale
                // the moment another writer touches its rows. That staleness is
                // the #1977 mechanism itself; a real surface reclaims its rows
                // on any full redraw (a resize, a mode change), and this is that
                // redraw made deterministic.
                t.clear().ok();
                t.draw(|f| {
                    f.render_widget(ratatui::widgets::Paragraph::new(filled), f.area());
                })
                .ok();
            };
            paint_prompt(&mut prompt, 0);

            match crate::panel::make_terminal(PANEL_TEST_HEIGHT) {
                Ok(mut terminal) => {
                    // The panel fills EVERY row with its body marker, so the
                    // assertion cannot pass on a row the prompt happens not to
                    // cover.
                    let body = (0..PANEL_TEST_HEIGHT)
                        .map(|r| format!("{PANEL_BODY_SENTINEL} r{r}"))
                        .collect::<Vec<_>>()
                        .join("\n");
                    terminal
                        .draw(|f| {
                            f.render_widget(ratatui::widgets::Paragraph::new(body), f.area());
                        })
                        .expect("the panel draws");
                    println!("{PANEL_SENTINEL_OK}");
                    // The prompt keeps repainting while the panel is open —
                    // which is what the rich input does, and what makes the
                    // panel body invisible rather than merely misplaced.
                    for tick in 1..=5 {
                        paint_prompt(&mut prompt, tick);
                        std::thread::sleep(Duration::from_millis(20));
                    }
                }
                Err(e) => println!("PANEL-FAILED {e}"),
            }
            use std::io::Write as _;
            std::io::stdout().flush().ok();
            stop.store(true, std::sync::atomic::Ordering::Relaxed);
            drop(reader);
        }

        // #2573 hardening: `panel::drive(.., None)` — the path with no cockpit
        // to lend rows — while another component holds the bottom 3 rows.
        // `NEWT_PANEL_HELD_MOVE` makes the holder follow a terminal resize to
        // the new bottom, as the rich prompt does.
        "panel_held" => {
            use std::io::Write as _;
            use std::sync::{Arc, Mutex};
            let holder = crate::inline_viewport::lease_bottom_rows(
                HOLDER_ROWS,
                newt_core::tty::OnCollision::Refuse,
            )
            .expect("the holder takes the bottom rows first");
            let paint = |top: u16, park: u16| {
                let mut out = std::io::stdout();
                for r in 0..HOLDER_ROWS {
                    let _ = write!(out, "\x1b[{};1H{HOLDER_SENTINEL}-{r}", top + r + 1);
                }
                // Park the cursor (1-based `park`): a `println!` from row 24
                // would scroll the holder's rows up a line and blur what is
                // measured.
                let _ = write!(out, "\x1b[{park};1H");
                let _ = out.flush();
            };
            let (_, rows) = crossterm::terminal::size().expect("size");
            // Where a real prompt leaves the cursor: the row a panel opens on,
            // just above the holder. A terminal that answers the cursor query
            // (`drive_held(.., true)`) places the panel here.
            paint(
                rows - HOLDER_ROWS,
                rows - HOLDER_ROWS - PANEL_TEST_HEIGHT + 1,
            );
            let holder = Arc::new(Mutex::new(holder));
            if std::env::var_os("NEWT_PANEL_HELD_MOVE").is_some() {
                let holder = holder.clone();
                std::thread::spawn(move || loop {
                    std::thread::sleep(Duration::from_millis(10));
                    let (_, now) = crossterm::terminal::size().expect("size");
                    if now == rows {
                        continue;
                    }
                    let top = now.saturating_sub(HOLDER_ROWS);
                    let moved = holder.lock().unwrap().relocate(
                        newt_core::tty::Region::Rows {
                            top,
                            height: HOLDER_ROWS,
                        },
                        newt_core::tty::OnCollision::Refuse,
                    );
                    paint(top, 1);
                    println!("HOLDER_MOVED:{moved}");
                    return;
                });
            }
            // No newline: it would move the cursor off the panel's row.
            print!("HOLDER_READY");
            std::io::stdout().flush().ok();
            let result = crate::panel::drive(&mut HeldPanel, PANEL_TEST_HEIGHT, None);
            // Say how `drive` ended, whichever way it did.
            println!(
                "DRIVE_RESULT:{:?}",
                result.as_ref().map_err(ToString::to_string)
            );
            println!("HOLDER_STILL_HOLDS:{:?}", holder.lock().unwrap().region());
            hold();
        }

        other => panic!("unknown child mode {other:?}"),
    }
}

/// Everything the parent needs to judge one lifecycle.
struct Outcome {
    raw_after: bool,
    /// Was the pty raw while the panel was up? **The control**: a test that
    /// never observed raw mode would prove restoration vacuously.
    raw_during: bool,
    /// Did the child actually ASK where the cursor was, and did the parent
    /// answer? #1950's control: a panel that opened because nothing ever
    /// queried the terminal would prove nothing about a contended query.
    answered_cursor_query: bool,
    screen: String,
}

fn drive(mode: &str) -> Outcome {
    let pty = Pty::open();
    let mut child = std::process::Command::new(
        std::env::current_exe().expect("the test binary re-invokes itself"),
    )
    .args(["--exact", CHILD_TEST, "--ignored", "--nocapture"])
    .env("NEWT_PANEL_PTY_CHILD", mode)
    .stdin(pty.slave_stdio())
    .stdout(pty.slave_stdio())
    .stderr(std::process::Stdio::null())
    .spawn()
    .expect("spawn the pty child");

    let mut screen = String::new();
    let mut answered_cursor_query = false;
    let raw_during = {
        let deadline = Instant::now() + REACH_TIMEOUT;
        loop {
            screen.push_str(&pty.screen());
            // Play the terminal: answer DSR (`ESC[6n`) with a cursor report,
            // the way a real emulator does. A bare pty answers nothing on its
            // own, so without this every inline surface would time out here
            // for a reason that has nothing to do with what is under test.
            if !answered_cursor_query && screen.contains("\u{1b}[6n") {
                pty.type_in("\u{1b}[3;1R");
                answered_cursor_query = true;
            }
            if pty.is_raw() {
                break true;
            }
            if Instant::now() >= deadline {
                break false;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
    };

    // Release the child's blocking read so it proceeds to its exit path.
    pty.type_in("x");

    let _status = std::thread::scope(|scope| {
        let waiter = scope.spawn(|| wait_for_child(&mut child, EXIT_TIMEOUT));
        while !waiter.is_finished() {
            screen.push_str(&pty.screen());
            if !answered_cursor_query && screen.contains("\u{1b}[6n") {
                pty.type_in("\u{1b}[3;1R");
                answered_cursor_query = true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        waiter.join().expect("child reaper thread")
    });
    // Drain whatever the exit path emitted.
    std::thread::sleep(Duration::from_millis(80));
    screen.push_str(&pty.screen());

    Outcome {
        raw_after: pty.is_raw(),
        raw_during,
        answered_cursor_query,
        screen,
    }
}

fn assert_restored(what: &str, out: &Outcome) {
    assert!(
        out.raw_during,
        "{what}: the pty was never raw — the test proved nothing about \
         restoration, because it never observed the state being restored FROM"
    );
    assert!(
        !out.raw_after,
        "{what}: the terminal was left in RAW mode — an operator's shell would \
         be unusable. This is kernel state via tcgetattr, not an inference \
         from escape bytes; screen={:?}",
        out.screen
    );
}

#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_clean_panel_close_leaves_the_terminal_exactly_as_it_was_found() {
    assert_restored("clean", &drive("clean"));
}

/// **The failure path**, and the whole reason #1889 exists: the one that turns
/// a bug into a wrecked session, and the one least likely to be noticed
/// missing, because the happy path always restores.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panic_while_the_panel_is_up_still_restores_the_terminal() {
    assert_restored("panic", &drive("panic"));
}

/// **The error-return path**: how a failed `terminal.draw` leaves `run`.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn an_error_return_while_the_panel_is_up_still_restores_the_terminal() {
    assert_restored("error", &drive("error"));
}

/// **#1950: the panel opens even when another consumer takes the cursor
/// reply.**
///
/// `Viewport::Inline` makes ratatui ask the terminal where the cursor is, and
/// the reply comes back on the *input* stream — so any other reader of that
/// stream can take it first. The operator hit this on `/backends` inside a
/// multiplexer; newt's own interrupt watcher is another candidate. Before the
/// fix, `Terminal::with_options` returned
/// `"The cursor position could not be read within a normal duration"` and the
/// panel did not open at all.
///
/// **This test must fail against `17a89c91`.** It was written by making the
/// failure — a real competing reader on a real pty — rather than by asserting
/// on a mocked error, because a mocked error proves only that the rescue
/// branch compiles.
///
/// It was ALSO measured against the pre-#1924 shape: raw mode taken through
/// `crossterm::terminal::enable_raw_mode` (so crossterm's own raw flag is
/// correctly set) fails here identically. The cursor query is not a raw-mode
/// problem, and syncing that flag fixes nothing.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panel_still_opens_when_the_cursor_reply_is_taken_by_another_reader() {
    let out = drive("inline_contended");

    // CONTROL 1: the query really was asked and really was answered. Without
    // this, a pass could mean the inline viewport never queried at all.
    assert!(
        out.answered_cursor_query,
        "the child never emitted ESC[6n, so nothing was contended and this \
         test proved nothing; screen={:?}",
        out.screen
    );
    // CONTROL 2: the guard was actually engaged, as the other tests require.
    assert!(
        out.raw_during,
        "the pty was never raw; screen={:?}",
        out.screen
    );

    assert!(
        !out.screen.contains("PANEL-FAILED"),
        "the panel refused to open — this is #1950; screen={:?}",
        out.screen
    );
    assert!(
        out.screen.contains(PANEL_SENTINEL_OK) && out.screen.contains(PANEL_SENTINEL),
        "the panel did not DRAW. Opening without drawing leaves the operator \
         the same blank screen the bug produced; screen={:?}",
        out.screen
    );
    // And it still hands the terminal back, which is what the rest of this
    // file exists to hold.
    assert!(
        !out.raw_after,
        "the terminal was left RAW; screen={:?}",
        out.screen
    );
}

/// **#1977, red-first.** Two bottom-anchored inline viewports own the same
/// rows and neither knows about the other, so the prompt's repaints erase the
/// panel's body. The operator saw a top border and nothing under it, and the
/// content only reached the screen when Esc's teardown flushed it to
/// scrollback.
///
/// **This asserts on the GRID, not the byte stream, and that distinction is
/// the whole test.** `drive` accumulates every byte the child emits, and the
/// panel's body IS emitted — that is why it appeared in scrollback. A
/// `screen.contains(BODY)` assertion would therefore pass against the very
/// defect it claims to catch. Only replaying the cursor motions and asking
/// what is left on the screen can tell "drawn" from "drawn then overpainted".
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panel_body_survives_a_competing_bottom_anchored_viewport() {
    let out = drive("inline_overpainted");
    let grid = screen_grid(&out.screen);

    // CONTROL 1: the panel opened at all. Without this a "body missing"
    // failure could just be #1950 regressing.
    assert!(
        out.screen.contains(PANEL_SENTINEL_OK),
        "the panel never opened, so this says nothing about overpainting; \
         screen={:?}",
        out.screen
    );
    // CONTROL 2: the competing viewport really painted. If the prompt never
    // reached the screen there was no contention and a pass is meaningless.
    assert!(
        grid.iter().any(|line| line.contains(PROMPT_SENTINEL)),
        "the competing prompt never painted, so nothing contended; grid={grid:#?}"
    );
    // CONTROL 3: the body was emitted. This separates "never drawn" from
    // "drawn and then overpainted" — the second is #1977, the first is not.
    assert!(
        out.screen.contains(PANEL_BODY_SENTINEL),
        "the panel never drew its body; that is a different bug; screen={:?}",
        out.screen
    );

    // THE PROPERTY: emitted is not enough. It has to still be on the screen.
    assert!(
        grid.iter().any(|line| line.contains(PANEL_BODY_SENTINEL)),
        "the panel body was overpainted by the prompt viewport — #1977. It \
         reached the terminal (control 3) and is gone from the screen; \
         grid={grid:#?}"
    );
}

/// Drive `panel::drive(.., None)` beside a holder of the bottom 3 rows. The
/// panel starts at 6 rows on a 24-row terminal. Ctrl-Z asks for the whole
/// screen and Shift-Up for one more row; then, if `shrink_to` is set, the
/// terminal shrinks to that many rows (and the panel must then be the given
/// height) (`holder_follows`: the holder moves to
/// the new bottom first, as the rich prompt does) and Esc closes.
fn drive_held(shrink_to: Option<(u16, usize)>, holder_follows: bool, answer_dsr: bool) -> Held {
    let pty = Pty::open();
    pty.resize(24, 80);
    let baseline = pty.termios_snapshot();
    let mut command = std::process::Command::new(
        std::env::current_exe().expect("the test binary re-invokes itself"),
    );
    command
        .args(["--exact", CHILD_TEST, "--ignored", "--nocapture"])
        .env("NEWT_PANEL_PTY_CHILD", "panel_held")
        .env("TERM", "xterm-256color")
        .stdin(pty.slave_stdio())
        .stdout(pty.slave_stdio())
        .stderr(std::process::Stdio::null());
    if holder_follows {
        command.env("NEWT_PANEL_HELD_MOVE", "1");
    }
    let mut child = command.spawn().expect("spawn the pty child");
    // The command still owns dups of the slave: drop them, or EOF never comes.
    drop(command);
    let panel_height = |repaint: &str| {
        let rows = screen_grid(repaint);
        let top = rows.iter().position(|row| row.contains('╭'));
        let bottom = rows.iter().rposition(|row| row.contains('╰'));
        top.zip(bottom).map(|(top, bottom)| bottom - top + 1)
    };
    let mut pump = Pump::new(&pty, answer_dsr, 24);
    let result = (|| -> Result<(), String> {
        let step = |pump: &mut Pump, what: &str, act: &dyn Fn(&mut Pump), want: usize| {
            pump.mark();
            act(pump);
            let Some(repaint) = pump.until("╯") else {
                return Err(format!("no repaint after: {what}"));
            };
            match panel_height(&repaint) {
                Some(height) if height == want => Ok(()),
                other => Err(format!(
                    "{what}: height {other:?}, want {want}: {repaint:?}"
                )),
            }
        };
        if pump.until("HOLDER_READY").is_none() || pump.until("╯").is_none() {
            return Err("the panel never painted".into());
        }
        // With `answer_dsr` off the DSR is unanswered on purpose and the panel
        // anchors on its lease; on, the parent answers it from where its own
        // replay says the cursor is, as a real terminal does.
        step(
            &mut pump,
            "Ctrl-Z is refused, the panel keeps its rows",
            &|_| pty.type_in("\x1a"),
            6,
        )?;
        step(
            &mut pump,
            "Shift-Up grows one row",
            &|_| pty.type_in("\x1b[1;2A"),
            7,
        )?;
        if let Some((rows, panel_rows)) = shrink_to {
            pump.rows = usize::from(rows);
            step(
                &mut pump,
                "the terminal shrinks",
                &|pump| {
                    pty.resize(rows, 80);
                    if holder_follows {
                        // The holder moves first; only then does the panel hear.
                        assert!(pump.until("HOLDER_MOVED:true").is_some());
                    }
                    signal_winch(child.id());
                },
                panel_rows,
            )?;
        }
        pump.mark();
        pty.type_in("\x1b");
        if pump.until("HOLDER_STILL_HOLDS").is_none() {
            return Err("Esc did not close the panel".into());
        }
        Ok(())
    })();
    // `drive` has returned by now, one way or another; the child says how.
    let returned = result.is_ok() || pump.until("HOLDER_STILL_HOLDS").is_some();
    let termios_restored = returned && pty.termios_snapshot() == baseline;
    pump.pump();
    pty.type_in("\n");
    if result.is_err() {
        let _ = child.kill();
    }
    let status = wait_for_child(&mut child, EXIT_TIMEOUT);
    let mut transcript = pump.transcript;
    transcript.push_str(&pty.screen_when_finished(status.is_some()));
    Held {
        result,
        stream: transcript,
        termios_restored,
        exit_ok: status.is_some_and(|status| status.success()),
    }
}

/// Drains the child's output into one transcript, and (when `answer_dsr`)
/// answers each cursor-position query from a replay of that transcript.
pub(crate) struct Pump<'a> {
    pty: &'a Pty,
    pub(crate) transcript: String,
    /// Where the current step's bytes start in `transcript`.
    mark: usize,
    answer_dsr: bool,
    /// The screen height the replay scrolls at.
    pub(crate) rows: usize,
    /// Transcript offset just past the last query answered.
    answered_through: usize,
}

impl<'a> Pump<'a> {
    pub(crate) fn new(pty: &'a Pty, answer_dsr: bool, rows: usize) -> Self {
        Self {
            pty,
            transcript: String::new(),
            mark: 0,
            answer_dsr,
            rows,
            answered_through: 0,
        }
    }

    pub(crate) fn pump(&mut self) {
        const QUERY: &str = "\x1b[6n";
        let chunk = self.pty.screen();
        // Search from just before the chunk, so a query split across two
        // reads is still answered once.
        let from = self.transcript.len().saturating_sub(QUERY.len() - 1);
        self.transcript.push_str(&chunk);
        if !self.answer_dsr {
            return;
        }
        let mut at = from;
        while let Some(found) = self.transcript[at..].find(QUERY) {
            let query = at + found;
            if query >= self.answered_through {
                let (_, (row, col)) = crate::interaction_view_pty_test::erasing_screen(
                    &self.transcript[..query],
                    self.rows,
                );
                self.pty.type_in(&format!("\x1b[{};{}R", row + 1, col + 1));
                self.answered_through = query + QUERY.len();
            }
            at = query + QUERY.len();
        }
    }

    pub(crate) fn mark(&mut self) {
        self.pump();
        self.mark = self.transcript.len();
    }

    /// Pump until `needle` appears in what arrived since the last `mark`, and
    /// return that text. The mark then moves past it.
    pub(crate) fn until(&mut self, needle: &str) -> Option<String> {
        let deadline = Instant::now() + REACH_TIMEOUT;
        loop {
            self.pump();
            if let Some(at) = self.transcript[self.mark..].find(needle) {
                let end = self.mark + at + needle.len();
                let text = self.transcript[self.mark..end].to_string();
                self.mark = end;
                return Some(text);
            }
            if Instant::now() >= deadline {
                return None;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
    }
}

/// What `drive_held` saw.
struct Held {
    /// The steps the parent drove: every height, and Esc closing the panel.
    result: Result<(), String>,
    stream: String,
    /// Kernel termios equal to the baseline once `drive` returned.
    termios_restored: bool,
    exit_ok: bool,
}

/// The holder's rows are still there after everything: replayed with erases.
fn assert_holder_untouched(held: &Held, rows: usize, top: usize) {
    // Up to where `drive` returned: the child's later `println!`s and the test
    // harness's own lines scroll the screen and are not part of the property.
    let stream = held.stream.split("DRIVE_RESULT").next().unwrap_or_default();
    let screen = crate::interaction_view_pty_test::erasing_grid(stream, rows);
    for r in 0..usize::from(HOLDER_ROWS) {
        assert!(
            screen[top + r].contains(&format!("{HOLDER_SENTINEL}-{r}")),
            "holder row {r} was overwritten or erased: {screen:#?}\nstream={:?}",
            held.stream
        );
    }
}

/// #2573 hardening, the no-cockpit path: with the bottom 3 rows held (a
/// `Refuse` lease), Ctrl-Z cannot take the screen and Shift-Up grows only
/// into free rows. The panel stays open at what it was granted, and Esc closes
/// with `drive` returning `Ok` and termios restored.
///
/// The holder's rows must also be untouched (#2573 hardening): the panel
/// erases only its own rows.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panel_beside_a_row_holder_stays_open_at_its_granted_height() {
    let held = drive_held(None, false, false);
    assert!(held.result.is_ok(), "{:?}: {:?}", held.result, held.stream);
    assert!(
        held.stream.contains("DRIVE_RESULT:Ok(false)"),
        "drive did not return Ok: {:?}",
        held.stream
    );
    assert!(
        held.termios_restored,
        "termios not restored: {:?}",
        held.stream
    );
    assert!(held.exit_ok, "{:?}", held.stream);
    assert_holder_untouched(&held, 24, 21);
}

/// The same beside a holder that follows a terminal shrink 24 → 10 rows, as
/// the rich prompt does. The panel (7 rows after the grow) fits exactly above
/// the holder's new rows, so it stays open at 7 and closes `Ok`.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panel_beside_a_row_holder_survives_a_terminal_shrink_that_still_fits() {
    let held = drive_held(Some((10, 7)), true, false);
    assert!(held.result.is_ok(), "{:?}: {:?}", held.result, held.stream);
    assert!(
        held.stream.contains("HOLDER_MOVED:true"),
        "{:?}",
        held.stream
    );
    assert!(
        held.stream.contains("DRIVE_RESULT:Ok(false)"),
        "drive did not return Ok: {:?}",
        held.stream
    );
    assert!(
        held.termios_restored,
        "termios not restored: {:?}",
        held.stream
    );
    assert!(held.exit_ok, "{:?}", held.stream);
}

/// A shrink that leaves fewer free rows above the holder than the panel had
/// (24 → 8, holder on rows 5..7, so 5 free) degrades the panel to what fits.
/// Before, `relet` found neither 7 rows nor the granted 7 and `drive` returned
/// `Err("another surface already owns these rows")`, closing the panel.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_shrink_that_leaves_no_room_degrades_the_panel_instead_of_closing_it() {
    let held = drive_held(Some((8, 5)), true, false);
    assert!(held.result.is_ok(), "{:?}: {:?}", held.result, held.stream);
    assert!(
        held.stream.contains("DRIVE_RESULT:Ok(false)"),
        "drive did not return Ok: {:?}",
        held.stream
    );
    assert!(
        held.termios_restored,
        "termios not restored: {:?}",
        held.stream
    );
    assert!(held.exit_ok, "{:?}", held.stream);
}

/// #2591 review: a terminal that ANSWERS the cursor query (production shape)
/// places a fresh inline viewport at the cursor. Growing the panel one row
/// (Shift-Up) rebuilds it; with the cursor parked at the OLD top the 7-row
/// viewport landed one row low and its clear erased the holder's first row.
/// The park is on the new lease's top, so the holder's rows survive.
#[serial_test::serial(interaction_pty)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_grow_beside_a_holder_leaves_its_rows_when_the_terminal_answers_the_cursor_query() {
    let held = drive_held(None, false, true);
    assert!(held.result.is_ok(), "{:?}: {:?}", held.result, held.stream);
    assert!(
        held.stream.contains("DRIVE_RESULT:Ok(false)"),
        "drive did not return Ok: {:?}",
        held.stream
    );
    assert!(held.stream.contains("\x1b[6n"), "control: the panel asked");
    assert_holder_untouched(&held, 24, 21);
}