use super::*;
use crate::cockpit::test_tty::{
echoes, is_canonical, mode_diff, modes_equal, set_canonical_echo, termios_of, TestTty,
};
const CTRL_C: &[u8] = &[0x03];
#[cfg(feature = "live-spill")]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn completed_spill_pager_owns_the_terminal_through_resize_and_close() {
crate::interaction_view_pty_test::drive_cockpit_pager();
}
#[cfg(feature = "live-spill")]
pub(crate) fn cockpit_pager_case() {
assert!(unsafe { libc::setsid() } >= 0);
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 0) }, 0);
let before = termios_of(0);
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
cockpit
.screen
.insert_rows(vec![b"HISTORY_BEFORE_PAGER".to_vec()])
.unwrap();
cockpit.draw().unwrap();
let (to_ui, requests) = std::sync::mpsc::sync_channel(8);
let worker = std::thread::spawn(move || {
let mut remote = crate::session_worker::RemoteSurface::new(to_ui);
let archive = crate::completed_spill::CompletedSpillArchive::default();
let body = (0..50)
.map(|i| format!("spill-row-{i:02}\n"))
.collect::<String>();
let id = archive.retain(&body);
crate::transcript_pager::run_output_pager(&archive.get(id).unwrap(), &mut remote, true)
.unwrap();
println!("PAGER_RETURNED");
match remote.read_line("after pager").unwrap() {
ReadOutcome::Line(line) => assert_eq!(line, "after-view", "modal keys leaked"),
other => panic!("pager left the editor unusable: {other:?}"),
}
});
cockpit.run(&requests).unwrap();
worker.join().unwrap();
assert!(
modes_equal(&before, &termios_of(0)),
"exact termios restore"
);
println!("PAGER_RESTORED");
std::io::stdin().read_line(&mut String::new()).unwrap();
}
#[cfg(feature = "rich-tui")]
#[serial_test::serial(tty_arbiter, prompt_stdin)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn psyche_panel_draws_once_in_the_lent_rows_under_the_cockpit() {
crate::interaction_view_pty_test::drive_cockpit_psyche();
}
#[cfg(feature = "rich-tui")]
pub(crate) fn cockpit_psyche_case() {
use crate::chat::InputSurface as _;
assert!(unsafe { libc::setsid() } >= 0);
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 0) }, 0);
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
cockpit.draw().unwrap();
let (to_ui, requests) = std::sync::mpsc::sync_channel(8);
let worker = std::thread::spawn(move || {
let mut remote = crate::session_worker::RemoteSurface::new(to_ui);
let window = remote.open_panel(crate::session_worker::PanelMode::Inline(
crate::config_panel::PANEL_HEIGHT,
));
assert!(window.is_some(), "the cockpit must lend rows for /psyche");
let seed = crate::config_panel::PanelSeed {
via: "/psyche",
personas: Vec::new(),
current_persona: None,
backend: Some("sol".to_string()),
base_initiative: newt_core::initiative::Initiative::default(),
initiative_from_family: false,
models: None,
current_model: String::new(),
personality: Default::default(),
};
let outcome = crate::config_panel::run(
seed,
|_, _, _| crate::config_panel::SaveResult::Failed("unused".into()),
window,
)
.unwrap();
println!("PSYCHE_RETURNED:{outcome:?}");
match remote.read_line("after panel").unwrap() {
ReadOutcome::Line(line) => assert_eq!(line, "after-psyche", "panel keys leaked"),
other => panic!("panel left the editor unusable: {other:?}"),
}
});
cockpit.run(&requests).unwrap();
worker.join().unwrap();
println!("PSYCHE_RESTORED");
std::io::stdin().read_line(&mut String::new()).unwrap();
}
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn buffered_paste_enter_and_interrupt_do_not_need_another_key() {
crate::interaction_view_pty_test::drive_cockpit_buffered_input();
}
pub(crate) fn cockpit_buffered_input_case() {
let result = std::panic::catch_unwind(buffered_input_case);
if let Err(error) = result {
let message = error
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| error.downcast_ref::<&str>().copied())
.unwrap_or("non-text panic");
println!("BUFFERED_INPUT_FAILURE:{message}");
std::panic::resume_unwind(error);
}
}
fn buffered_input_case() {
let tty = TestTty::install();
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
let prefetch = |bytes: &[u8]| {
let _stdin = newt_core::tty::try_watch_stdin().expect("prefetch owns stdin");
tty.type_bytes(bytes);
assert!(event::poll(Duration::from_secs(1)).unwrap());
let mut fd = libc::pollfd {
fd: libc::STDIN_FILENO,
events: libc::POLLIN,
revents: 0,
};
assert_eq!(
unsafe { libc::poll(&mut fd, 1, 0) },
0,
"input must already be buffered by crossterm"
);
};
let (reply, received) = std::sync::mpsc::sync_channel(1);
cockpit
.handle_request(SurfaceRequest::ReadLine {
prompt: "buffered input".into(),
reply,
})
.unwrap();
prefetch(b"\x1b[200~atomic paste\x1b[201~\r");
cockpit.poll_keys().unwrap();
assert!(
matches!(received.try_recv(), Ok(Ok(ReadOutcome::Line(line))) if line == "atomic paste"),
"buffered paste and Enter must submit without another terminal byte"
);
let cancel = Arc::new(AtomicBool::new(false));
cockpit
.handle_request(SurfaceRequest::TurnStarted {
cancel: Arc::clone(&cancel),
})
.unwrap();
#[cfg(feature = "live-spill")]
{
let archive = Arc::new(crate::completed_spill::CompletedSpillArchive::default());
archive.retain("operator inspection payload");
cockpit
.handle_request(SurfaceRequest::SetSpillArchive(archive))
.unwrap();
let (reply, inspected) = std::sync::mpsc::sync_channel(1);
cockpit
.handle_request(SurfaceRequest::ReadLine {
prompt: "inspect".into(),
reply,
})
.unwrap();
cockpit
.on_event(Event::Paste("unsent draft".into()))
.unwrap();
let draft = cockpit.editor.draft();
prefetch(b"\x1bOS\rqq");
cockpit.poll_keys().unwrap();
assert_eq!(cockpit.editor.draft(), draft);
assert!(cockpit.queued.is_empty());
assert!(matches!(
inspected.try_recv(),
Err(std::sync::mpsc::TryRecvError::Empty)
));
assert!(!cancel.load(Ordering::SeqCst));
}
prefetch(CTRL_C);
{
let _other_reader = newt_core::tty::try_watch_stdin().expect("another reader owns stdin");
cockpit.poll_keys().unwrap();
assert!(
!cancel.load(Ordering::SeqCst),
"the cockpit must respect stdin ownership"
);
}
cockpit.poll_keys().unwrap();
assert!(
cancel.load(Ordering::SeqCst),
"buffered Ctrl-C must interrupt without another terminal byte"
);
let saved = unsafe { libc::dup(libc::STDIN_FILENO) };
assert!(saved >= 0);
assert_eq!(unsafe { libc::close(libc::STDIN_FILENO) }, 0);
let closed = cockpit.poll_keys();
assert_eq!(unsafe { libc::dup2(saved, libc::STDIN_FILENO) }, 0);
unsafe { libc::close(saved) };
assert!(matches!(closed, Err(error) if error.kind() == io::ErrorKind::BrokenPipe));
}
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn the_clarification_modal_can_be_answered_pasted_into_and_escaped() {
crate::interaction_view_pty_test::drive_cockpit_clarification_input();
}
pub(crate) fn cockpit_clarification_input_case() {
let result = std::panic::catch_unwind(clarification_input_case);
if let Err(error) = result {
let message = error
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| error.downcast_ref::<&str>().copied())
.unwrap_or("non-text panic");
println!("CLARIFICATION_INPUT_FAILURE:{message}");
std::panic::resume_unwind(error);
}
}
fn clarification_input_case() {
let tty = TestTty::install();
let before = termios_of(0);
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
let (reply, received) = std::sync::mpsc::sync_channel(1);
let typer = tty.type_when_painted("CLAR_ROUND_ONE", b"1: keep the index\r");
cockpit
.handle_request(SurfaceRequest::PresentClarification {
batch: "CLAR_ROUND_ONE\n1: keep the index\n2: rebuild it".into(),
hint: "ordinal locks it".into(),
prompt: String::new(),
color: false,
verbose: false,
reply,
})
.unwrap();
assert!(
typer.join().expect("round-one prompt watcher"),
"the modal must reach the real terminal before input is sent"
);
assert!(
matches!(received.try_recv(), Ok(Ok(ReadOutcome::Line(line))) if line == "1: keep the index"),
"a typed ordinal + Enter must reach the clarification path, not \
literal bytes: {:?}",
received.try_recv()
);
let (reply2, received2) = std::sync::mpsc::sync_channel(1);
let typer2 = tty.type_when_painted(
"CLAR_ROUND_TWO",
b"\x1b[200~/discuss why the plan changed\x1b[201~\r",
);
cockpit
.handle_request(SurfaceRequest::PresentClarification {
batch: "CLAR_ROUND_TWO\n1: keep the index".into(),
hint: "ordinal locks it".into(),
prompt: String::new(),
color: false,
verbose: false,
reply: reply2,
})
.unwrap();
assert!(
typer2.join().expect("round-two prompt watcher"),
"the modal must reach the real terminal before input is sent"
);
assert!(
matches!(received2.try_recv(), Ok(Ok(ReadOutcome::Line(line))) if line == "/discuss why the plan changed"),
"a bracketed paste + Enter must reach the clarification path, not \
raw escape bytes: {:?}",
received2.try_recv()
);
let (reply3, received3) = std::sync::mpsc::sync_channel(1);
let typer3 = tty.type_when_painted("CLAR_ROUND_THREE", b"\x1b");
cockpit
.handle_request(SurfaceRequest::PresentClarification {
batch: "CLAR_ROUND_THREE\n1: keep the index".into(),
hint: "ordinal locks it".into(),
prompt: String::new(),
color: false,
verbose: false,
reply: reply3,
})
.unwrap();
assert!(
typer3.join().expect("round-three prompt watcher"),
"the modal must reach the real terminal before input is sent"
);
assert!(
matches!(received3.try_recv(), Ok(Ok(ReadOutcome::Line(line))) if line.is_empty()),
"Esc must dismiss to an ordinary (empty) answer, not trap the \
operator: {:?}",
received3.try_recv()
);
drop(cockpit);
assert!(
modes_equal(&before, &termios_of(0)),
"exact termios restore after the clarification modal"
);
println!("CLARIFICATION_INPUT_RESTORED");
}
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn bang_commands_lend_the_terminal_and_restore_the_editor() {
crate::interaction_view_pty_test::drive_cockpit_bang();
}
pub(crate) fn cockpit_bang_case() {
assert!(unsafe { libc::setsid() } >= 0);
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 0) }, 0);
let before = termios_of(0);
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let cockpit = Presenter::open(surface).expect("cockpit");
let (to_ui, requests) = std::sync::mpsc::sync_channel(8);
let worker = std::thread::spawn(move || {
let mut remote = crate::session_worker::RemoteSurface::new(to_ui);
for step in 0..8 {
let command = format!(
"printf 'BANG_READY_{step}>'; IFS= read -r answer; result=$?; \
printf '\\nBANG_READ_{step}:%s:%s\\n' \"$result\" \"$answer\"; exit $result"
);
if step < 4 {
remote.run_bang_escape(&command, false, false).unwrap();
} else {
let success = remote
.run_login_argv(&["/bin/sh".into(), "-c".into(), command], false, false)
.unwrap();
assert_eq!(success, matches!(step, 4 | 7));
}
assert_eq!(unsafe { libc::tcgetpgrp(0) }, unsafe { libc::getpgrp() });
println!("BANG_RETURN_{step}");
match remote.read_line("after bang").unwrap() {
ReadOutcome::Line(line) => assert_eq!(line, format!("next-{step}")),
other => panic!("bang left the editor unusable: {other:?}"),
}
}
});
cockpit.run(&requests).unwrap();
worker.join().unwrap();
assert!(
modes_equal(&before, &termios_of(0)),
"exact termios restore"
);
println!("BANG_RESTORED");
std::io::stdin().read_line(&mut String::new()).unwrap();
}
#[serial_test::serial(tty_arbiter, prompt_stdin)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn panel_release_resynchronizes_terminal_size_before_restoring_draft() {
crate::interaction_view_pty_test::drive_cockpit_resize();
}
pub(crate) fn panel_resize_case() {
let _tty = TestTty::install();
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
cockpit
.editor
.on_event(
Event::Paste("draft survives resize".into()),
&mut cockpit.screen,
)
.unwrap();
let draft = cockpit.editor.draft();
let (reply, received) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
let panel = std::thread::spawn(move || {
let window = received.recv().unwrap().expect("reserved panel");
let size = libc::winsize {
ws_row: 12,
ws_col: 46,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSWINSZ, &size) }, 0);
drop(window);
});
cockpit
.handle_request(SurfaceRequest::Panel {
mode: PanelMode::Inline(8),
reply,
})
.unwrap();
panel.join().unwrap();
assert_eq!((cockpit.screen.cols, cockpit.screen.rows), (46, 12));
assert_eq!(cockpit.editor.draft(), draft);
assert!(!cockpit.chat_inactive);
assert!(cockpit.screen.viewport_rect().bottom() <= 12);
}
#[serial_test::serial(tty_arbiter, prompt_stdin)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn an_open_panel_follows_the_terminal_through_shrink_and_grow() {
crate::interaction_view_pty_test::drive_cockpit_live_resize();
}
pub(crate) fn panel_live_resize_case() {
let _tty = TestTty::install();
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
cockpit
.editor
.on_event(
Event::Paste("draft survives a live resize".into()),
&mut cockpit.screen,
)
.unwrap();
let draft = cockpit.editor.draft();
let (reply, received) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
let panel = std::thread::spawn(move || {
let window = received.recv().unwrap().expect("reserved panel");
let resize = |rows: u16, cols: u16| {
let size = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSWINSZ, &size) }, 0);
};
resize(12, 46);
window.remeasure(None);
assert_eq!(
window.area(),
ratatui::layout::Rect::new(0, 4, 46, 8),
"shrink"
);
resize(30, 100);
window.remeasure(None);
assert_eq!(
window.area(),
ratatui::layout::Rect::new(0, 22, 100, 8),
"grow"
);
window.remeasure(None);
assert_eq!(
window.area(),
ratatui::layout::Rect::new(0, 22, 100, 8),
"no change"
);
drop(window);
});
cockpit
.handle_request(SurfaceRequest::Panel {
mode: PanelMode::Inline(8),
reply,
})
.unwrap();
panel.join().unwrap();
assert_eq!((cockpit.screen.cols, cockpit.screen.rows), (100, 30));
assert_eq!(cockpit.editor.draft(), draft);
assert!(!cockpit.chat_inactive);
assert!(cockpit.screen.viewport_rect().bottom() <= 30);
let (reply, received) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
let alternate = std::thread::spawn(move || {
let window = received.recv().unwrap().expect("alternate-screen loan");
let size = libc::winsize {
ws_row: 20,
ws_col: 70,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSWINSZ, &size) }, 0);
window.remeasure(None);
assert_eq!(window.area(), ratatui::layout::Rect::new(0, 0, 70, 20));
window.remeasure(Some(5));
assert_eq!(
window.area(),
ratatui::layout::Rect::new(0, 0, 70, 20),
"a height request does not shrink a full-screen loan"
);
drop(window);
});
cockpit
.handle_request(SurfaceRequest::Panel {
mode: PanelMode::AlternateScreen,
reply,
})
.unwrap();
alternate.join().unwrap();
assert_eq!((cockpit.screen.cols, cockpit.screen.rows), (70, 20));
assert_eq!(cockpit.editor.draft(), draft);
}
#[serial_test::serial(tty_arbiter, prompt_stdin)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_same_size_resize_does_not_move_the_block_away_from_its_content_2632() {
let _tty = TestTty::install();
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
let content_top = cockpit.screen.top;
let block_h = cockpit.screen.block_h;
let (cols, rows) = (cockpit.screen.cols, cockpit.screen.rows);
cockpit.on_event(Event::Resize(cols, rows)).unwrap();
assert_eq!(
cockpit.screen.top, content_top,
"a same-size resize must not recreate the #2441 gap"
);
let shrunk = rows - 2;
assert!(
content_top + block_h <= shrunk,
"test precondition: the block still fits after the shrink"
);
cockpit.on_event(Event::Resize(cols, shrunk)).unwrap();
assert_eq!(
cockpit.screen.top, content_top,
"a partial shrink that still fits must not re-anchor to the bottom"
);
}
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_driven_panel_survives_resizes_keeps_its_selection_and_hands_back_the_draft() {
crate::interaction_view_pty_test::drive_cockpit_panel_loop();
}
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn closing_a_panel_leaves_the_transcript_above_it_and_none_of_the_panel() {
crate::interaction_view_pty_test::drive_cockpit_panel_close();
}
pub(crate) fn cockpit_panel_loop_case() {
assert!(unsafe { libc::setsid() } >= 0);
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 0) }, 0);
let before = termios_of(0);
let surface = crate::rich_input::RichSurface::new(None).expect("rich surface");
let mut cockpit = Presenter::open(surface).expect("cockpit");
cockpit
.screen
.insert_rows(vec![b"HISTORY_ABOVE_PANEL".to_vec()])
.unwrap();
cockpit
.editor
.on_event(
Event::Paste("draft-survives-the-panel".into()),
&mut cockpit.screen,
)
.unwrap();
cockpit.draw().unwrap();
let (to_ui, requests) = std::sync::mpsc::sync_channel(8);
let worker = std::thread::spawn(move || {
let mut remote = crate::session_worker::RemoteSurface::new(to_ui);
let window = crate::chat::InputSurface::open_panel(&mut remote, PanelMode::Inline(8));
let rows = (0..30).map(|n| format!("panel-row-{n:02}")).collect();
let mut panel = crate::lines_panel::LinesPanel::new("loop", rows);
crate::panel::drive(&mut panel, 8, window.as_ref()).unwrap();
drop(window);
println!("PANEL_CLOSED");
match remote.read_line("after panel").unwrap() {
ReadOutcome::Line(line) => assert_eq!(line, "draft-survives-the-panel"),
other => panic!("the panel lost the draft: {other:?}"),
}
});
cockpit.run(&requests).unwrap();
worker.join().unwrap();
assert!(
modes_equal(&before, &termios_of(0)),
"exact termios restore"
);
println!("PANEL_LOOP_RESTORED");
std::io::stdin().read_line(&mut String::new()).unwrap();
}
#[serial_test::serial(tty_arbiter, prompt_stdin)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn the_cockpit_owns_the_terminal_correctly_and_gives_it_back() {
crate::interaction_view_pty_test::drive_cockpit_acceptance();
}
pub(crate) fn cockpit_acceptance_case() {
let result = std::panic::catch_unwind(acceptance_case);
if let Err(error) = result {
let message = error
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| error.downcast_ref::<&str>().copied())
.unwrap_or("non-text panic");
println!("COCKPIT_ACCEPTANCE_FAILURE:{message}");
std::panic::resume_unwind(error);
}
}
fn acceptance_case() {
let tty = TestTty::install();
assert!(
unsafe { libc::setsid() } >= 0,
"setsid for the controlling tty"
);
assert_eq!(unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 0) }, 0);
unsafe { libc::signal(libc::SIGHUP, libc::SIG_IGN) };
newt_core::tty::set_interrupt_pending(false);
set_canonical_echo(0);
let before = termios_of(0);
assert!(
is_canonical(0) && echoes(0),
"precondition: the terminal starts as a shell hands it over"
);
let dir = std::env::temp_dir().join(format!("newt-cockpit-acceptance-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let surface =
crate::rich_input::RichSurface::new(Some(dir.join("history"))).expect("rich surface");
{
let mut cockpit = match Presenter::open(surface) {
Ok(p) => p,
Err(e) => panic!(
"cockpit failed to open: {e}; master saw {:?}",
tty.painted()
),
};
assert!(!is_canonical(0), "the cockpit runs the terminal raw");
assert!(!echoes(0), "and the kernel is not echoing over the editor");
use crossterm::style::{Attribute, Color};
use newt_core::tty::theme::{self, Role, Theme};
let mut custom = Theme::builtin().overlaid(
"test",
&[
(
Role::Thinking,
Color::Rgb {
r: 12,
g: 34,
b: 56,
},
),
(
Role::AgentText,
Color::Rgb {
r: 78,
g: 90,
b: 123,
},
),
],
);
let mut thinking = custom.style(Role::Thinking);
thinking.attributes.set(Attribute::Italic);
custom.set_style(Role::Thinking, thinking);
let first_style = custom.ansi(Role::Thinking);
let answer_style = custom.ansi(Role::AgentText);
theme::set_active(custom);
let before_transcript = tty.painted().len();
newt_core::tty::Notice::new(
newt_core::tty::Level::Thinking,
"",
"THOUGHT_FIRST\n\nTHOUGHT_SECOND\nThought for 5s",
)
.emit(
newt_core::tty::LineCaps::Own,
newt_core::tty::Sink::Stdout,
true,
);
writeln!(
io::stdout(),
"{}",
newt_core::agentic::render_markdown(
"FINAL_RESPONSE",
newt_core::agentic::RenderOpts {
color: true,
cols: 80
},
)
)
.unwrap();
io::stdout().flush().unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
cockpit.drain_pty().expect("drain styled transcript");
if tty.wait_for_painted_after(
before_transcript,
"FINAL_RESPONSE\x1b[0m",
Duration::from_millis(5),
) {
break;
}
assert!(
Instant::now() < deadline,
"styled transcript was not painted"
);
}
let painted = tty.painted();
let transcript = &painted[before_transcript..];
assert!(transcript.contains(&format!("{first_style}THOUGHT_FIRST\x1b[0m")));
for thought in ["THOUGHT_SECOND", "Thought for 5s"] {
assert!(
transcript.contains(&format!("\x1b[3;38;2;12;34;56m{thought}\x1b[0m")),
"thought style lost for {thought}: {transcript:?}"
);
}
assert!(transcript.contains(&format!("{answer_style}FINAL_RESPONSE\x1b[0m")));
let cancel = Arc::new(AtomicBool::new(false));
cockpit
.handle_request(SurfaceRequest::TurnStarted {
cancel: Arc::clone(&cancel),
})
.expect("a turn starts");
tty.type_bytes(CTRL_C);
cockpit.poll_keys().expect("first Ctrl-C");
assert!(
cancel.load(Ordering::SeqCst),
"first press trips the cancel the session races against"
);
assert_eq!(
newt_core::tty::interrupt_presses(),
1,
"first press raises the acknowledgment the operator sees"
);
tty.type_bytes(CTRL_C);
cockpit.poll_keys().expect("second Ctrl-C");
assert_eq!(
newt_core::tty::interrupt_presses(),
2,
"the second press is acknowledged at press time"
);
cockpit
.handle_request(SurfaceRequest::TurnEnded)
.expect("the turn ends");
assert!(
!newt_core::tty::interrupt_pending(),
"ending the turn clears the acknowledgment, so the next turn \
does not open already showing it"
);
cockpit
.screen
.insert_rows(vec![b"transcript row".to_vec(); 24])
.expect("push the cockpit block to the bottom");
cockpit.draw().expect("seat the bottom-anchored block");
let stale_top = cockpit.screen.top;
{
let (editor, screen) = (&mut cockpit.editor, &mut cockpit.screen);
editor
.on_event(Event::Paste("draft survives".into()), screen)
.expect("prefill the mounted draft");
}
let draft = cockpit.editor.draft();
cockpit
.handle_request(SurfaceRequest::SetBackgroundJobs(vec![
crate::chat::BackgroundJob::start("indexing repository"),
]))
.expect("queue a chrome row before the modal");
assert!(cockpit.dirty, "the queued chrome has not drawn yet");
let expected_status_rows = cockpit.status_rows();
let expected_editor_rows = cockpit.editor.wanted_rows(
cockpit.screen.cols,
cockpit.screen.rows,
&cockpit.surface.chrome(),
);
let expected_block_h =
(expected_editor_rows + expected_status_rows).clamp(1, cockpit.screen.rows.max(1));
let expected_top = if stale_top + expected_block_h > cockpit.screen.rows {
cockpit.screen.rows - expected_block_h
} else {
stale_top
};
assert!(
expected_top < stale_top,
"the regression needs the pending chrome row to move the block: \
stale={stale_top}, expected={expected_top}"
);
let definition = crate::permissions::free_text_form("Cockpit modal visible?");
let interaction = newt_core::interaction_surface::SurfaceInteraction::blocking(definition);
let requested_rows =
crate::interaction_view::requested_rows(&interaction, cockpit.screen.cols);
let expected_reservation =
plan_modal_reservation(expected_top, cockpit.screen.rows, requested_rows);
assert!(
!expected_reservation.chat_visible,
"the modal must occlude the composer: {expected_reservation:?}"
);
let before_modal_round = tty.painted().len();
let (reply, answer) = std::sync::mpsc::sync_channel(1);
let (saw_box, while_modal) = std::thread::scope(|scope| {
let typer = scope.spawn(|| {
let saw_prompt = tty.wait_for_painted_after(
before_modal_round,
"visible?",
std::time::Duration::from_secs(2),
);
let prompt_end = tty.painted()[before_modal_round..]
.find("visible?")
.map_or(before_modal_round, |at| {
before_modal_round + at + "visible?".len()
});
let saw_box = saw_prompt
&& tty.wait_for_painted_after(
prompt_end,
"╯",
std::time::Duration::from_secs(2),
);
let painted = tty.painted();
tty.type_bytes(b"yes\r");
(saw_box, painted)
});
cockpit
.handle_request(SurfaceRequest::Interact {
interaction: Box::new(interaction),
reply,
})
.expect("present the interaction");
typer.join().expect("prompt watcher")
});
assert!(
saw_box,
"the modal must reach the real terminal before input is sent; painted: {while_modal:?}"
);
let modal_delta = &while_modal[before_modal_round..];
let erase = modal_cleanup_bytes(expected_reservation.start).expect("modal erase bytes");
let erase = String::from_utf8(erase).expect("erase bytes are UTF-8");
let erased_at = modal_delta.find(&erase).unwrap_or_else(|| {
panic!("the modal must erase the covered composer before painting its box: {modal_delta:?}")
});
let modal_grid = tests_pty::screen_grid(&modal_delta[erased_at..]);
let modal_visible = modal_grid.join("\n");
for text in ["╭", "╮", "╰", "╯", "Cockpit modal visible?"] {
assert!(
modal_visible.contains(text),
"the blocking question must be enclosed and readable before input; missing {text:?}: {modal_visible}"
);
}
assert!(
!modal_visible.contains("draft survives"),
"the normal composer must not be drawn into the blocking window: {modal_visible}"
);
assert_eq!(
answer.recv().expect("interaction answer"),
newt_core::HumanQuestionOutcome::Answer("yes".into())
);
assert_eq!(cockpit.editor.draft(), draft, "the chat draft survives");
assert!(!cockpit.chat_inactive, "keyboard focus returns to chat");
assert_eq!(
cockpit.screen.top, expected_top,
"the pending layout is applied before modal reservation"
);
let provisional = tty.painted();
let prompt_at = provisional
.find("visible?")
.expect("modal body reached the terminal");
let mut show = Vec::new();
queue!(show, crossterm::cursor::Show).expect("show-cursor bytes");
let show = String::from_utf8(show).expect("show bytes are UTF-8");
assert!(
tty.wait_for_painted_after(prompt_at, &show, std::time::Duration::from_secs(2)),
"the chat cursor was not restored after the modal closed (waited 2s): {:?}",
tty.painted()
);
let painted = tty.painted();
let mut expected_move = Vec::new();
queue!(expected_move, MoveTo(0, expected_reservation.start))
.expect("cursor-placement bytes");
let expected_move = String::from_utf8(expected_move).expect("cursor bytes are UTF-8");
assert!(
painted[..prompt_at].contains(&expected_move),
"the modal was not placed over the composer; expected {expected_move:?}, painted: {painted:?}"
);
let mut hide = Vec::new();
queue!(hide, crossterm::cursor::Hide).expect("hide-cursor bytes");
let hide = String::from_utf8(hide).expect("hide bytes are UTF-8");
let before_modal = &painted[..prompt_at];
assert!(
before_modal.rfind(&hide) > before_modal.rfind(&show),
"the mounted chat cursor must recede before the modal takes focus: {painted:?}"
);
assert!(
painted[prompt_at..].contains(&show),
"the chat cursor was not restored after the modal closed: {painted:?}"
);
let mut enable_wrap = Vec::new();
queue!(enable_wrap, EnableLineWrap).expect("enable-wrap bytes");
let enable_wrap = String::from_utf8(enable_wrap).expect("wrap bytes are UTF-8");
assert!(
!modal_delta[erased_at..].contains(&enable_wrap),
"a modal must keep terminal autowrap disabled so a long answer cannot spill into chat: {:?}",
&modal_delta[erased_at..]
);
assert!(
painted[prompt_at..].contains(&enable_wrap),
"autowrap must be restored once the modal closes: {:?}",
&painted[prompt_at..]
);
let slash_definition = crate::permissions::free_text_form("Slash commands stay in chat?");
let slash_top = cockpit.screen.top;
let slash_block_h = cockpit.screen.block_h;
let slash_rows = cockpit.screen.rows;
let slash_cols = cockpit.screen.cols;
let before_slash = tty.painted().len();
let slash_typer = tty.type_when_painted("chat?", b"/help\r");
let (slash_reply, slash_answer) = std::sync::mpsc::sync_channel(1);
cockpit
.handle_request(SurfaceRequest::Interact {
interaction: Box::new(
newt_core::interaction_surface::SurfaceInteraction::blocking(slash_definition),
),
reply: slash_reply,
})
.expect("present the slash-command interaction");
assert!(
slash_typer.join().expect("slash prompt watcher"),
"the slash-command prompt must be visible before input"
);
assert_eq!(
slash_answer.recv().expect("slash-command outcome"),
newt_core::HumanQuestionOutcome::Cancelled,
"a slash command is chat intent, not a modal answer"
);
let notice_row = crate::permissions::SLASH_COMMAND_PROMPT_NOTICE
.as_bytes()
.to_vec();
let notice_rows = wrap_row(¬ice_row, usize::from(slash_cols));
let (committed_notice, _) =
render_insert(slash_top, slash_block_h, slash_rows, ¬ice_rows)
.expect("durable notice insertion plan");
let committed_notice = String::from_utf8(committed_notice).expect("notice bytes are UTF-8");
assert!(
tty.wait_for_painted_after(
before_slash,
&committed_notice,
std::time::Duration::from_secs(2)
),
"slash guidance was not committed above the cockpit viewport (waited 2s): {:?}",
&tty.painted()[before_slash..]
);
let painted_after_slash = tty.painted();
let slash_delta = &painted_after_slash[before_slash..];
assert!(
slash_delta.contains(&committed_notice),
"slash guidance was not committed above the cockpit viewport: {slash_delta:?}"
);
assert_eq!(
cockpit.editor.draft(),
draft,
"backing out of a modal keeps the mounted chat draft"
);
let window = newt_core::tty::Terminal::suspend_for_prompt(
newt_core::tty::TerminalTaker::RichSurfaceModal,
);
{
let _reader = newt_core::tty::modal_prompt_controls(&window)
.expect("the modal takes the terminal from under the cockpit");
assert!(
!is_canonical(0),
"the modal's read must be non-canonical, or a keypress \
waits for Enter the operator does not know to press"
);
assert!(
!echoes(0),
"the kernel must not echo the answer over the prompt"
);
}
drop(window);
assert!(
!is_canonical(0),
"the cockpit's raw mode survives the modal"
);
let (panel_reply, panel_window) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
let painter = std::thread::spawn(move || {
let window = panel_window
.recv()
.expect("the presenter answers the panel request")
.expect("a mounted cockpit has rows to lend");
let mut term = window.terminal().expect("a terminal over the lent rows");
term.draw(|f| {
f.render_widget(
ratatui::widgets::Paragraph::new("PANEL BODY ON THE REAL TERMINAL"),
f.area(),
);
})
.expect("the panel paints");
drop(term);
drop(window);
});
let panel_plan = plan_modal_reservation(cockpit.screen.top, cockpit.screen.rows, 6);
assert!(
!panel_plan.chat_visible,
"a blocking panel must occlude the normal composer: {panel_plan:?}"
);
let before_panel = tty.painted().len();
cockpit
.handle_request(SurfaceRequest::Panel {
mode: PanelMode::Inline(6),
reply: panel_reply,
})
.expect("the presenter lends the panel its rows");
painter.join().expect("panel painter");
assert!(
tty.wait_for_painted_after(before_panel, "TERMINAL", std::time::Duration::from_secs(2)),
"the panel's bytes never reached the real terminal (waited 2s): {:?}",
&tty.painted()[before_panel..]
);
let panel_delta = tty.painted()[before_panel..].to_string();
let mut panel_move = Vec::new();
queue!(panel_move, MoveTo(0, panel_plan.start)).expect("panel cursor bytes");
let panel_move = String::from_utf8(panel_move).expect("panel cursor bytes are UTF-8");
assert!(
panel_delta.contains(&panel_move),
"the panel must be placed in its reserved rows over chat; \
expected {panel_move:?}, painted: {panel_delta:?}"
);
for word in ["PANEL", "BODY", "REAL", "TERMINAL"] {
assert!(
panel_delta.contains(word),
"the panel's bytes must reach the real terminal, not the \
cockpit's fd 1 capture; missing {word:?}: {panel_delta:?}"
);
}
assert!(
!cockpit.chat_inactive,
"keyboard focus returns to chat when the panel window drops"
);
assert_eq!(
cockpit.editor.draft(),
draft,
"the chat draft survives a panel"
);
assert!(
!is_canonical(0),
"the cockpit's raw mode survives the panel"
);
for mode in [PanelMode::Inline(6), PanelMode::AlternateScreen] {
let (early_reply, early_window) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
let early = std::thread::spawn(move || {
drop(early_window.recv().expect("the presenter answers"));
});
cockpit
.handle_request(SurfaceRequest::Panel {
mode,
reply: early_reply,
})
.expect("an undrawn panel still returns");
early.join().expect("early-drop panel");
assert!(
!cockpit.chat_inactive,
"focus returns when a panel drops its window without drawing"
);
let (orphan_reply, orphan_window) =
std::sync::mpsc::sync_channel::<Option<crate::session_worker::PanelWindow>>(1);
drop(orphan_window);
cockpit
.handle_request(SurfaceRequest::Panel {
mode,
reply: orphan_reply,
})
.expect("a vanished session is not a presenter error");
assert!(
!cockpit.chat_inactive,
"a panel request nobody received must not leave chat dimmed"
);
}
assert_eq!(
cockpit.editor.draft(),
draft,
"the chat draft survives both panel failure paths"
);
let before_repaint = tty.painted().len();
{
let (editor, screen) = (&mut cockpit.editor, &mut cockpit.screen);
editor
.on_event(Event::Paste(" and still alive".into()), screen)
.expect("the mounted editor still takes input");
}
cockpit
.draw()
.expect("the cockpit still paints after a failed panel");
assert!(
tty.wait_for_painted_after(before_repaint, "alive", std::time::Duration::from_secs(2)),
"the cockpit block is still live after the panel failure paths \
(waited 2s): {:?}",
&tty.painted()[before_repaint..]
);
}
assert!(is_canonical(0), "canonical mode restored on teardown");
assert!(echoes(0), "echo restored on teardown");
assert!(
modes_equal(&before, &termios_of(0)),
"every termios mode field restored exactly, not approximately — \
this is the assertion an emitted-escape-bytes check cannot make; \
differs: {}",
mode_diff(&before, &termios_of(0))
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
#[ignore = "real-PTY acceptance tier; weekly, release, and scoped PTY CI only"]
fn a_panic_restores_the_real_termios_through_the_modes_guard() {
let _tty = TestTty::install();
let _ = crossterm::terminal::disable_raw_mode();
set_canonical_echo(0);
let before = termios_of(0);
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(|| {
struct Held {
_restore: crate::RestoreOnDrop<fn()>,
_raw: newt_core::tty::raw_mode::RawModeGuard,
}
let _held = Held {
_restore: crate::RestoreOnDrop {
restore: restore_terminal_modes,
},
_raw: newt_core::tty::raw_mode::RawModeGuard::enter().expect("raw"),
};
assert!(!is_canonical(0), "precondition: raw mode taken");
panic!("turn exploded while the terminal was raw");
});
std::panic::set_hook(hook);
assert!(
result.is_err(),
"the panic must propagate, not be swallowed"
);
assert!(
is_canonical(0),
"canonical mode restored through the unwind"
);
assert!(echoes(0), "echo restored through the unwind");
assert!(
modes_equal(&before, &termios_of(0)),
"every termios mode field restored exactly after a panic; differs: {}",
mode_diff(&before, &termios_of(0))
);
}