#[test]
fn protocol_mode_vetoes_every_prompt() {
assert!(
!super::prompts_permitted(true),
"fd 1 may be a JSON-RPC wire; no prompt may reach it"
);
}
#[test]
fn and_a_process_outside_protocol_mode_may_prompt() {
assert!(
super::prompts_permitted(false),
"an ordinary terminal session is the ONE shape that prompts — if \
this fails the veto test above is vacuous"
);
}
#[test]
fn a_vetoed_window_refuses_to_ask_or_read_and_drops_notices() {
let window = super::PromptWindow::vetoed(super::PromptOutput::Stdout);
assert!(
window.ask("question > ").is_err(),
"ask must refuse rather than report a question it never wrote"
);
let mut buf = String::new();
let read = window.read_line_into(&mut buf);
assert!(
read.is_err(),
"read must ERROR, not return Ok(0) — EOF is a deliberate empty \
answer from a human, and forging one is not failing closed: {read:?}"
);
assert!(buf.is_empty(), "nothing may land in the caller's buffer");
assert!(
window.notice("fyi").is_ok(),
"a notice is informational and is dropped silently, which is why \
it differs from ask"
);
}
#[test]
fn and_an_unvetoed_window_is_not_refused() {
let window = super::PromptWindow::test_stub();
assert!(window.ask("").is_ok(), "an ordinary window may ask");
assert!(window.notice("").is_ok(), "an ordinary window may narrate");
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn an_explicit_prompt_output_routes_ask_and_notice_to_that_file() {
let output = tempfile::NamedTempFile::new().expect("prompt output file");
let c = counter();
let dynamic: Arc<dyn Ephemeral> = c.clone();
Terminal::register(9_002, &dynamic);
let window = Terminal::suspend_for_prompt_to(
output.reopen().expect("independent prompt output handle"),
TerminalTaker::PlainCliConfirm,
);
assert_eq!(
c.erased.load(Ordering::SeqCst),
1,
"the alternate output must not bypass prompt arbitration"
);
assert!(suspended(), "the alternate output quiesces other writers");
assert!(
prompt_stdin_active(),
"the alternate output still owns prompt stdin"
);
assert!(
!window.output_is_terminal(),
"a regular-file destination must select the non-TTY modal path"
);
window.ask("question > ").expect("write the question");
window.notice("narration").expect("write the notice");
drop(window);
assert!(!suspended(), "dropping the window resumes other writers");
assert!(!prompt_stdin_active(), "dropping the window releases stdin");
assert_eq!(c.restored.load(Ordering::SeqCst), 1);
assert_eq!(
std::fs::read_to_string(output.path()).expect("read routed prompt bytes"),
"question > narration\n"
);
}
#[cfg(unix)]
#[serial_test::serial(tty_arbiter)]
#[test]
fn an_explicit_pty_output_is_detected_and_written_as_a_terminal() {
use std::io::Read as _;
use std::os::fd::FromRawFd as _;
let mut master_fd = -1;
let mut slave_fd = -1;
let opened = unsafe {
libc::openpty(
&mut master_fd,
&mut slave_fd,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(opened, 0, "open the grounding PTY");
let (mut master, output) =
unsafe { (File::from_raw_fd(master_fd), File::from_raw_fd(slave_fd)) };
let window = Terminal::suspend_for_prompt_to(output, TerminalTaker::PlainCliConfirm);
assert!(
window.output_is_terminal(),
"the saved terminal file must select the interactive modal path"
);
window.ask("direct prompt").expect("write to the PTY slave");
let mut painted = [0_u8; "direct prompt".len()];
master
.read_exact(&mut painted)
.expect("read the explicitly routed terminal bytes");
assert_eq!(&painted, b"direct prompt");
drop(window);
}
#[test]
fn a_vetoed_explicit_output_still_emits_zero_bytes() {
let output = tempfile::NamedTempFile::new().expect("vetoed prompt output file");
let window = PromptWindow::vetoed(PromptOutput::File(
output.reopen().expect("independent prompt output handle"),
));
assert!(window.ask("question").is_err());
assert!(window.notice("narration").is_ok());
drop(window);
assert_eq!(
std::fs::read_to_string(output.path()).expect("read vetoed prompt output"),
""
);
}
#[test]
fn the_default_prompt_window_keeps_the_process_stdout_route() {
let window = PromptWindow::test_stub();
assert!(matches!(&window.output, PromptOutput::Stdout));
assert_eq!(
window.output_is_terminal(),
io::stdout().is_terminal(),
"the legacy window must keep probing the stream it writes"
);
}
use super::*;
use std::sync::atomic::AtomicUsize;
struct Counter {
erased: AtomicUsize,
restored: AtomicUsize,
}
impl Ephemeral for Counter {
fn erase(&self) {
self.erased.fetch_add(1, Ordering::SeqCst);
}
fn restore(&self) {
self.restored.fetch_add(1, Ordering::SeqCst);
}
}
fn counter() -> Arc<Counter> {
Arc::new(Counter {
erased: AtomicUsize::new(0),
restored: AtomicUsize::new(0),
})
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_line_admits_exactly_one_writer() {
let first = Terminal::lease_with_caps(LineCaps::Own, Sink::Stdout)
.expect("an Own capability yields the line");
assert!(
Terminal::lease_with_caps(LineCaps::Own, Sink::Stdout).is_none(),
"a second writer must NOT get the line while the first holds it"
);
drop(first);
let third = Terminal::lease_with_caps(LineCaps::Own, Sink::Stdout);
assert!(third.is_some(), "the line is reusable once released");
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn no_capability_means_no_lease_and_no_bytes() {
assert!(Terminal::lease_with_caps(LineCaps::None, Sink::Stdout).is_none());
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn suspending_erases_every_ephemeral_then_restores() {
let c = counter();
let dynamic: Arc<dyn Ephemeral> = c.clone();
Terminal::register(9_001, &dynamic);
assert_eq!(c.erased.load(Ordering::SeqCst), 0);
{
let w = Terminal::suspend_for_prompt(TerminalTaker::PlainCliConfirm);
assert_eq!(
c.erased.load(Ordering::SeqCst),
1,
"the ephemeral must be erased before the window is handed out"
);
assert!(suspended(), "the ticker must see the suspend flag");
assert_eq!(c.restored.load(Ordering::SeqCst), 0);
drop(w);
}
assert!(!suspended(), "the flag clears when the window drops");
assert_eq!(c.restored.load(Ordering::SeqCst), 1);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_live_prompt_window_makes_painting_a_no_op() {
let lease = Terminal::lease_with_caps(LineCaps::Own, Sink::Stdout).expect("lease");
let w = Terminal::suspend_for_prompt(TerminalTaker::PlainCliConfirm);
lease.paint(|_w| Ok(()));
assert!(
!lease.painted.load(Ordering::SeqCst),
"a paint during a prompt must be dropped, not deferred onto the question"
);
drop(w);
lease.paint(|_w| Ok(()));
assert!(
lease.painted.load(Ordering::SeqCst),
"painting resumes once the window is gone"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn erase_is_idempotent() {
let lease = Terminal::lease_with_caps(LineCaps::Own, Sink::Stdout).expect("lease");
lease.paint(|_w| Ok(()));
assert!(lease.painted.load(Ordering::SeqCst));
lease.erase();
assert!(!lease.painted.load(Ordering::SeqCst));
lease.erase(); assert!(!lease.painted.load(Ordering::SeqCst));
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn nested_prompts_hold_stdin_until_the_outermost_releases() {
assert!(
!prompt_stdin_active(),
"test starts with no active prompt stdin owner"
);
{
let _outer = StdinToken::acquire();
assert!(
try_watch_stdin().is_none(),
"the watcher cannot read while a prompt owns stdin"
);
assert!(prompt_stdin_active());
{
let _nested = StdinToken::acquire();
assert!(prompt_stdin_active(), "nested prompts keep ownership");
}
assert!(
prompt_stdin_active(),
"dropping one nested guard must not release stdin early"
);
}
assert!(
!prompt_stdin_active(),
"prompt stdin ownership must clear when the last guard drops"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn watcher_read_token_blocks_prompt_entry_until_the_read_finishes() {
let watcher = try_watch_stdin().expect("watcher acquires idle stdin");
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let prompt = std::thread::spawn(move || {
let _prompt = StdinToken::acquire();
let _ = entered_tx.send(());
});
while super::STDIN_TOKEN_WAITERS.load(std::sync::atomic::Ordering::SeqCst) == 0 {
assert!(
entered_rx.try_recv().is_err(),
"prompt entered during the watcher's protected read"
);
std::thread::yield_now();
}
drop(watcher);
crate::test_guard::recv_guarded(
&entered_rx,
"the prompt entering once the watcher releases stdin",
);
prompt.join().unwrap();
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn watcher_and_prompt_exclude_each_other_on_stdin() {
assert!(!prompt_stdin_active());
let watcher = try_watch_stdin().expect("the watcher acquires idle stdin");
assert!(
try_watch_stdin().is_none(),
"the watcher's read token is exclusive"
);
drop(watcher);
let token = StdinToken::acquire();
assert!(prompt_stdin_active());
assert!(
try_watch_stdin().is_none(),
"the watcher must not read while a prompt owns stdin"
);
drop(token);
assert!(!prompt_stdin_active());
assert!(try_watch_stdin().is_some(), "released on drop");
}
#[test]
fn the_seal_has_exactly_two_doors_and_both_gates_sit_below_the_fork() {
let src = include_str!("arbiter.rs");
let production = src.split("\n#[cfg(test)]").next().unwrap_or("");
assert!(
production.len() > 1000,
"the production cut read nothing; every count below would be vacuous"
);
assert_eq!(
production.matches("pub fn suspend_for_prompt(").count(),
1,
"the stdout door"
);
assert_eq!(
production.matches("pub fn suspend_for_prompt_to(").count(),
1,
"the File door (#1959)"
);
assert_eq!(
production
.matches("Self::suspend_for_prompt_with_output(")
.count(),
2,
"BOTH public doors must delegate to the one private builder — a \
third door, or a door that built a PromptWindow itself, would \
bypass the gates below"
);
let veto = "prompts_permitted(super::caps::protocol_mode())";
let acquire = "StdinToken::acquire()";
assert_eq!(production.matches(veto).count(), 1, "one veto, one place");
assert_eq!(
production.matches(acquire).count(),
1,
"one acquire, one place"
);
let fork = production
.find("fn suspend_for_prompt_with_output")
.expect("the private builder");
assert!(
production.find(veto).is_some_and(|at| at > fork),
"the protocol veto was hoisted ABOVE the fork — it would then \
cover only the door it sits in, and the other would prompt on a \
JSON-RPC wire"
);
assert!(
production.find(acquire).is_some_and(|at| at > fork),
"the stdin token was hoisted ABOVE the fork — one door would then \
ask without exclusive stdin"
);
}
#[cfg(unix)]
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_file_door_takes_the_same_exclusive_stdin_token() {
assert!(!prompt_stdin_active(), "stdin must start idle");
let sink = std::fs::OpenOptions::new()
.write(true)
.open("/dev/null")
.expect("/dev/null");
let window = Terminal::suspend_for_prompt_to(sink, TerminalTaker::PlainCliConfirm);
assert!(
prompt_stdin_active(),
"the File door must take the prompt's stdin token, like the stdout door"
);
assert!(
try_watch_stdin().is_none(),
"and hold it EXCLUSIVELY — the turn watcher must not read underneath it"
);
drop(window);
assert!(!prompt_stdin_active(), "released on drop");
assert!(try_watch_stdin().is_some(), "the watcher may read again");
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_test_stub_arbitrates_nothing() {
let w = PromptWindow::test_stub();
assert!(!suspended());
drop(w);
assert!(!suspended());
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn blocked_is_emitted_after_stdin_acquisition_and_unblocked_on_drop() {
use crate::lifecycle::LifecycleEvent;
let log: Arc<Mutex<Vec<(LifecycleEvent, bool)>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&log);
let me = std::thread::current().id();
let sub = crate::lifecycle::subscribe(move |event| {
if std::thread::current().id() == me {
sink.lock()
.unwrap()
.push((event.event.clone(), prompt_stdin_active()));
}
});
{
let _stub = PromptWindow::test_stub();
}
assert!(
log.lock().unwrap().is_empty(),
"the stub must not emit lifecycle events"
);
let w = Terminal::suspend_for_prompt(TerminalTaker::PlainCliConfirm);
drop(w);
drop(sub);
assert_eq!(
*log.lock().unwrap(),
vec![
(LifecycleEvent::Blocked, true),
(LifecycleEvent::Unblocked, false)
],
"Blocked is emitted exactly once, with stdin already owned \
(post-acquire, not intent); Unblocked on drop, after stdin is released"
);
}
fn rows(top: u16, height: u16) -> Region {
Region::Rows { top, height }
}
#[test]
fn regions_intersect_only_when_they_share_a_row() {
assert!(rows(10, 3).intersects(rows(12, 2)), "overlapping");
assert!(rows(12, 2).intersects(rows(10, 3)), "and symmetrically");
assert!(
!rows(10, 3).intersects(rows(13, 2)),
"adjacent is not overlapping"
);
assert!(!rows(13, 2).intersects(rows(10, 3)), "and symmetrically");
assert!(Region::WholeScreen.intersects(rows(0, 1)));
assert!(rows(40, 1).intersects(Region::WholeScreen));
assert!(Region::WholeScreen.intersects(Region::WholeScreen));
assert!(!rows(10, 0).intersects(rows(10, 3)));
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_mint_refuses_rows_another_writer_holds() {
let held = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("first take");
assert_eq!(held.region(), rows(18, 6));
assert!(
Terminal::lease_region(rows(20, 4), OnCollision::Refuse).is_none(),
"two writers were granted the same rows — this is #1977"
);
let elsewhere =
Terminal::lease_region(rows(2, 4), OnCollision::Refuse).expect("clear rows are granted");
assert_eq!(elsewhere.region(), rows(2, 4));
drop(held);
drop(elsewhere);
let reclaimed =
Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("released rows return");
assert_eq!(reclaimed.region(), rows(18, 6));
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn shift_opens_above_the_holder_rather_than_through_it() {
let prompt = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("prompt");
let panel = Terminal::lease_region(rows(18, 6), OnCollision::Shift).expect("panel shifts");
assert_eq!(
panel.region(),
rows(12, 6),
"#1977: the panel must open ABOVE the prompt's rows, not over them"
);
assert!(
!panel.region().intersects(prompt.region()),
"the shifted region still overlaps"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn shift_refuses_when_there_is_no_room_above() {
let _floor = Terminal::lease_region(rows(0, 4), OnCollision::Refuse).expect("floor");
assert!(
Terminal::lease_region(rows(2, 6), OnCollision::Shift).is_none(),
"shifting off the top of the screen must refuse, not wrap"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn the_whole_screen_collides_with_everything_and_shifts_nowhere() {
let _rowsy = Terminal::lease_region(rows(10, 2), OnCollision::Refuse).expect("some rows");
assert!(
Terminal::lease_region(Region::WholeScreen, OnCollision::Refuse).is_none(),
"the alternate screen takes every row and cannot share"
);
assert!(
Terminal::lease_region(Region::WholeScreen, OnCollision::Shift).is_none(),
"whole-screen has nowhere to shift to"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn suspend_holder_takes_the_rows_the_caller_already_quiesced() {
let _held = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("holder");
let taken = Terminal::lease_region(rows(18, 6), OnCollision::SuspendHolder)
.expect("a caller that quiesced the holder takes the rows");
assert_eq!(taken.region(), rows(18, 6));
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_lease_relocates_in_place_and_still_respects_other_holders() {
let _other = Terminal::lease_region(rows(0, 4), OnCollision::Refuse).expect("other");
let mut moving = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("moving");
assert!(
moving.relocate(rows(10, 6), OnCollision::Refuse),
"a clear move must succeed"
);
assert_eq!(moving.region(), rows(10, 6));
assert!(
!moving.relocate(rows(2, 4), OnCollision::Refuse),
"relocating onto another holder was allowed"
);
assert_eq!(
moving.region(),
rows(10, 6),
"a refused move must leave the lease holding what it held"
);
assert!(
moving.relocate(rows(10, 8), OnCollision::Refuse),
"a lease may resize in place"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn an_inner_region_returns_without_disturbing_the_outer_one() {
let prompt = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("prompt");
{
let panel = Terminal::lease_region(rows(18, 6), OnCollision::Shift).expect("panel");
assert_eq!(panel.region(), rows(12, 6));
assert!(
Terminal::lease_region(rows(12, 6), OnCollision::Refuse).is_none(),
"the panel's rows were re-let while it held them"
);
assert!(
Terminal::lease_region(rows(18, 6), OnCollision::Refuse).is_none(),
"the prompt's rows were re-let while it held them"
);
}
let reclaimed =
Terminal::lease_region(rows(12, 6), OnCollision::Refuse).expect("the panel's rows return");
assert_eq!(reclaimed.region(), rows(12, 6));
assert!(
Terminal::lease_region(rows(18, 6), OnCollision::Refuse).is_none(),
"closing the panel released the PROMPT's rows — restoring more than \
it took is the nested-modal defect"
);
drop(prompt);
drop(reclaimed);
}
fn place(
cursor: (u16, u16),
height: u16,
held: &[Region],
policy: OnCollision,
) -> Option<Placement> {
let held: Vec<(u64, Region)> = held
.iter()
.enumerate()
.map(|(i, r)| (i as u64 + 1, *r))
.collect();
place_below_cursor(cursor, 24, height, &held, policy)
}
fn placed(top: u16, height: u16, scroll: u16, return_to: (u16, u16)) -> Option<Placement> {
Some(Placement {
top,
height,
scroll,
return_to,
rows: 24,
})
}
#[test]
fn a_frame_opens_on_the_cursor_row_and_returns_the_cursor_there() {
assert_eq!(
place((0, 5), 6, &[], OnCollision::Shift),
placed(5, 6, 0, (0, 5))
);
}
#[test]
fn a_partial_transcript_row_survives_frame_erasure() {
for (cursor, expected) in [
((11, 5), placed(6, 6, 0, (11, 5))),
((11, 20), placed(18, 6, 3, (11, 17))),
((11, 23), placed(18, 6, 6, (11, 17))),
] {
let actual = place(cursor, 6, &[], OnCollision::Refuse).expect("placement");
let expected = expected.unwrap();
#[cfg(not(windows))]
{
let mut bytes = Vec::new();
blank_rows(
&mut super::LineWriter(&mut bytes),
actual.top,
actual.height,
)
.unwrap();
let expected_bytes: String = (expected.top..expected.top + expected.height)
.map(|row| format!("\x1b[{};1H\x1b[2K", row + 1))
.collect();
assert_eq!(
bytes,
expected_bytes.as_bytes(),
"cursor {cursor:?}: erase must start below committed text"
);
assert!(
!String::from_utf8(bytes)
.unwrap()
.contains(&format!("\x1b[{};1H\x1b[2K", actual.return_to.1 + 1)),
"the return row must not be erased on open or close"
);
}
assert_eq!(actual, expected);
}
}
#[test]
fn a_partial_row_limits_the_frame_to_the_remaining_screen() {
assert_eq!(
place((11, 5), 40, &[], OnCollision::Shift),
placed(1, 23, 5, (11, 0))
);
assert_eq!(
place_below_cursor((11, 0), 1, 1, &[], OnCollision::Refuse),
None
);
}
#[test]
fn a_frame_that_runs_off_the_bottom_scrolls_the_transcript_by_the_deficit() {
assert_eq!(
place((0, 20), 6, &[], OnCollision::Shift),
placed(18, 6, 2, (0, 18))
);
assert_eq!(
place((0, 18), 6, &[], OnCollision::Shift),
placed(18, 6, 0, (0, 18))
);
}
#[test]
fn a_frame_taller_than_the_screen_is_clamped_to_it() {
assert_eq!(
place((0, 5), 40, &[], OnCollision::Shift),
placed(0, 24, 5, (0, 0))
);
assert_eq!(
place((0, 5), 0, &[], OnCollision::Shift),
placed(5, 1, 0, (0, 5))
);
}
#[test]
fn a_holder_below_a_frame_that_fits_is_not_a_collision() {
let prompt = [rows(21, 3)];
for policy in [
OnCollision::Refuse,
OnCollision::Shift,
OnCollision::SuspendHolder,
] {
assert_eq!(
place((0, 5), 6, &prompt, policy),
placed(5, 6, 0, (0, 5)),
"{policy:?}"
);
}
}
#[test]
fn contested_rows_follow_the_policy_and_a_shift_still_returns_the_cursor() {
let prompt = [rows(21, 3)];
assert_eq!(place((0, 18), 6, &prompt, OnCollision::Refuse), None);
assert_eq!(
place((0, 18), 6, &prompt, OnCollision::SuspendHolder),
placed(18, 6, 0, (0, 18))
);
assert_eq!(
place((0, 18), 6, &prompt, OnCollision::Shift),
placed(15, 6, 0, (0, 18))
);
let above = [rows(0, 4)];
assert_eq!(place((0, 20), 6, &above, OnCollision::Refuse), None);
assert_eq!(
place((0, 20), 6, &above, OnCollision::Shift),
placed(18, 6, 0, (0, 20))
);
assert_eq!(place((0, 18), 6, &[rows(0, 24)], OnCollision::Shift), None);
}
#[test]
fn the_whole_screen_refuses_every_placement_but_a_declared_take() {
let screen = [Region::WholeScreen];
assert_eq!(place((0, 5), 6, &screen, OnCollision::Refuse), None);
assert_eq!(place((0, 5), 6, &screen, OnCollision::Shift), None);
assert_eq!(
place((0, 5), 6, &screen, OnCollision::SuspendHolder),
placed(5, 6, 0, (0, 5))
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_bare_acquisition_is_refused_while_a_surface_holds_rows() {
let _held = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("holder");
let window = Terminal::suspend_for_prompt(TerminalTaker::SlashForm);
assert!(
!prompt_stdin_active(),
"a refused acquisition must not have taken stdin"
);
assert!(
!suspended(),
"a refused acquisition must not have quiesced the holder"
);
assert!(
window.ask("question > ").is_err(),
"a refused window must not report a question it never wrote"
);
assert!(
window.read_line_into(&mut String::new()).is_err(),
"and must ERROR rather than synthesise an EOF nobody typed"
);
assert!(
window.notice("fyi").is_ok(),
"a notice is informational and is dropped silently, as under the veto"
);
drop(window);
assert!(
!suspended(),
"dropping a refused window must not clear a flag it never set"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn and_the_same_taker_acquires_when_nobody_holds_rows() {
let counter = counter();
let dynamic: Arc<dyn Ephemeral> = counter.clone();
Terminal::register(9_027, &dynamic);
let window = Terminal::suspend_for_prompt(TerminalTaker::SlashForm);
assert!(prompt_stdin_active(), "an uncontested taker owns stdin");
assert!(suspended(), "and quiesces every registered ephemeral");
assert_eq!(counter.erased.load(Ordering::SeqCst), 1);
assert!(window.ask("").is_ok(), "and may speak");
drop(window);
assert!(!prompt_stdin_active());
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_declared_row_taker_acquires_while_a_surface_holds_rows() {
let _held = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("holder");
let window = Terminal::suspend_for_prompt(TerminalTaker::CockpitModal);
assert!(
prompt_stdin_active(),
"a declared SuspendHolder taker must still get the terminal"
);
assert!(suspended(), "and must still quiesce the holder");
assert!(window.ask("").is_ok(), "and must be able to speak");
drop(window);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_refused_acquisition_is_still_counted() {
let _held = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("holder");
let before = prompt_windows_constructed();
drop(Terminal::suspend_for_prompt(TerminalTaker::SlashForm));
assert_eq!(
prompt_windows_constructed(),
before + 1,
"a refused attempt must remain visible in the counter"
);
}
#[test]
fn the_taker_table_is_complete_and_distinct() {
assert_eq!(
TerminalTaker::ALL.len(),
9,
"a variant was added without reaching ALL, which the registry test walks"
);
let mut names: Vec<&str> = TerminalTaker::ALL.iter().map(|t| t.name()).collect();
names.sort_unstable();
let listed = names.len();
names.dedup();
assert_eq!(names.len(), listed, "two takers share a name: {names:?}");
}
#[test]
fn no_taker_declares_a_shift() {
for taker in TerminalTaker::ALL {
assert_ne!(
taker.on_held_rows(),
OnCollision::Shift,
"`{}` declared Shift, which a whole-terminal take cannot honour",
taker.name()
);
}
}
#[test]
fn exactly_two_takers_take_rows_they_do_not_own() {
let deliberate: Vec<&str> = TerminalTaker::ALL
.iter()
.filter(|t| t.on_held_rows() == OnCollision::SuspendHolder)
.map(|t| t.name())
.collect();
assert_eq!(
deliberate,
vec!["CockpitModal", "PermissionAuthorization"],
"the set of takers that take rows they do not own may only shrink"
);
}
#[serial_test::serial(tty_arbiter)]
#[test]
fn a_forced_relocation_lands_where_a_checked_one_is_refused() {
let _other = Terminal::lease_region(rows(0, 4), OnCollision::Refuse).expect("other");
let mut block = Terminal::lease_region(rows(18, 6), OnCollision::Refuse).expect("block");
assert!(!block.relocate(rows(2, 4), OnCollision::Refuse));
assert_eq!(block.region(), rows(18, 6));
assert!(
block.relocate(rows(2, 4), OnCollision::SuspendHolder),
"a forced relocation must land, or the lease describes rows the \
writer has already left"
);
assert_eq!(block.region(), rows(2, 4));
assert!(
!block.relocate(rows(0, 4), OnCollision::Shift),
"a shifting relocation would move the lease somewhere the caller \
did not ask for"
);
assert_eq!(block.region(), rows(2, 4));
}
#[test]
fn quiet_query_waiter_has_priority_over_the_next_watcher_poll() {
let mut state = super::Inner::default();
assert!(state.watcher_can_read());
state.quiet_waiters = 1;
assert!(
!state.watcher_can_read(),
"a waiting query gets the next read turn"
);
state.quiet_waiters = 0;
assert!(state.watcher_can_read());
}
#[test]
#[serial_test::serial(tty_arbiter)]
fn quiet_query_timeout_releases_priority() {
let watcher = try_watch_stdin().unwrap();
assert!(super::StdinQuiet::take_within(std::time::Duration::ZERO).is_none());
assert_eq!(super::lock().quiet_waiters, 0);
drop(watcher);
let quiet = super::StdinQuiet::take_within(std::time::Duration::ZERO).unwrap();
assert_eq!(super::lock().quiet_waiters, 0);
assert!(try_watch_stdin().is_none());
drop(quiet);
assert!(try_watch_stdin().is_some());
}
#[test]
#[serial_test::serial(tty_arbiter)]
fn quiet_query_registers_priority_until_the_watcher_finishes() {
let watcher = try_watch_stdin().unwrap();
let (acquired_tx, acquired_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let query = std::thread::spawn(move || {
let quiet = super::StdinQuiet::take_within(crate::test_guard::HANG_GUARD);
acquired_tx.send(quiet.is_some()).unwrap();
crate::test_guard::recv_guarded(&release_rx, "release quiet query");
drop(quiet);
});
let deadline = std::time::Instant::now() + crate::test_guard::HANG_GUARD;
while super::lock().quiet_waiters == 0 {
assert!(std::time::Instant::now() < deadline, "query never queued");
std::thread::yield_now();
}
drop(watcher);
assert!(crate::test_guard::recv_guarded(
&acquired_rx,
"query takes the next turn"
));
assert_eq!(super::lock().quiet_waiters, 0);
assert!(try_watch_stdin().is_none());
release_tx.send(()).unwrap();
query.join().unwrap();
assert!(try_watch_stdin().is_some());
}