use std::panic::AssertUnwindSafe;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
#[test]
fn panic_hook_chain_restores_prior_hook_and_cookes_terminal() {
use std::sync::atomic::{AtomicBool, Ordering};
static PRIOR_HOOK_FIRED: AtomicBool = AtomicBool::new(false);
let original = std::panic::take_hook();
std::panic::set_hook(Box::new(|_info| {
PRIOR_HOOK_FIRED.store(true, Ordering::SeqCst);
}));
let prior = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen);
prior(info);
}));
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
panic!("simulated TUI main-loop panic");
}));
assert!(result.is_err(), "panic must propagate through catch_unwind");
assert!(
PRIOR_HOOK_FIRED.load(Ordering::SeqCst),
"the TUI panic hook must call the prior hook after restoring the terminal"
);
let raw_state = crossterm::terminal::is_raw_mode_enabled();
match raw_state {
Ok(false) | Err(_) => {}
Ok(true) => {
let _ = crossterm::terminal::disable_raw_mode();
std::panic::set_hook(original);
panic!("terminal left in raw mode after simulated TUI panic");
}
}
std::panic::set_hook(original);
}
#[test]
fn drop_runs_during_unwind_recon_8_invariant() {
use std::cell::Cell;
thread_local! { static FIRED: Cell<bool> = const { Cell::new(false) }; }
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
FIRED.with(|f| f.set(true));
}
}
FIRED.with(|f| f.set(false));
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _g = Guard;
panic!("trigger");
}));
assert!(r.is_err());
let fired = FIRED.with(|f| f.get());
assert!(
fired,
"Drop did not run during unwind — test profile may have been switched to `panic = \"abort\"`. \
Spec §4.3.5 / CC-Recon-1B-8 require unwind for the terminal-restoration guarantee."
);
}
#[test]
fn test_backend_renders_findings_layout_and_severity_letters() {
let mut term = Terminal::new(TestBackend::new(80, 24)).unwrap();
term.draw(|f| {
use ratatui::widgets::{Block, Borders, Paragraph};
let area = f.area();
f.render_widget(
Paragraph::new("▶ [H] Sample title\n [M] Other\n [L] Third").block(
Block::default()
.borders(Borders::ALL)
.title("Findings (3; 0 dismissed)"),
),
area,
);
})
.unwrap();
let buf = term.backend().buffer();
let mut found_h = false;
let mut found_marker = false;
for y in 0..buf.area().height {
for x in 0..buf.area().width {
let s = buf[(x, y)].symbol();
if s == "H" {
found_h = true;
}
if s == "▶" {
found_marker = true;
}
}
}
assert!(found_h, "list pane must render severity letter [H]");
assert!(found_marker, "selection marker '▶' must appear");
}
#[test]
fn empty_filtered_review_shortcut_message_shape() {
let m_dismissed = 3usize;
let msg = format!("No visible findings ({} dismissed).", m_dismissed);
assert_eq!(msg, "No visible findings (3 dismissed).");
assert!(msg.starts_with("No visible findings ("));
assert!(msg.ends_with("dismissed)."));
}
#[test]
fn tty_check_for_minus_minus_tui_runs_before_bundle_assembly() {
let exit_2_or_proceed = |is_tty: bool, tui: bool| -> Result<(), &'static str> {
if tui && !is_tty {
return Err("--tui requires an interactive terminal; omit --tui for stdout markdown");
}
Ok(())
};
assert!(exit_2_or_proceed(false, true).is_err());
assert!(exit_2_or_proceed(false, false).is_ok());
assert!(exit_2_or_proceed(true, true).is_ok());
}