#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use ftui_core::inline_mode::InlineStrategy;
use ftui_core::terminal_capabilities::TerminalCapabilities;
use ftui_render::buffer::Buffer;
use ftui_render::cell::Cell;
use ftui_runtime::{ScreenMode, TerminalWriter, UiAnchor};
const CURSOR_SAVE: &[u8] = b"\x1b7";
const CURSOR_RESTORE: &[u8] = b"\x1b8";
const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
const SYNC_END: &[u8] = b"\x1b[?2026l";
const ERASE_LINE: &[u8] = b"\x1b[2K";
const FULL_CLEAR: &[u8] = b"\x1b[2J";
const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
fn basic_caps() -> TerminalCapabilities {
TerminalCapabilities::basic()
}
fn modern_caps() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.true_color = true;
caps.sync_output = true;
caps.scroll_region = true;
caps
}
fn hybrid_caps() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.scroll_region = true;
caps
}
fn mux_caps() -> TerminalCapabilities {
let mut caps = TerminalCapabilities::basic();
caps.scroll_region = true;
caps.sync_output = true;
caps.in_tmux = true;
caps
}
fn dumb_caps() -> TerminalCapabilities {
TerminalCapabilities::dumb()
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
haystack
.windows(needle.len())
.filter(|w| *w == needle)
.count()
}
fn find_nth(haystack: &[u8], needle: &[u8], nth: usize) -> Option<usize> {
if nth == 0 {
return None;
}
let mut count = 0;
let mut i = 0;
while i + needle.len() <= haystack.len() {
if &haystack[i..i + needle.len()] == needle {
count += 1;
if count == nth {
return Some(i);
}
}
i += 1;
}
None
}
fn parse_cup_sequences(output: &[u8]) -> Vec<(u16, u16)> {
let mut results = Vec::new();
let mut i = 0;
while i + 2 < output.len() {
if output[i] == 0x1b && output[i + 1] == b'[' {
let mut j = i + 2;
let mut row: u16 = 0;
let mut saw_row = false;
while j < output.len() && output[j].is_ascii_digit() {
saw_row = true;
row = row
.saturating_mul(10)
.saturating_add(u16::from(output[j] - b'0'));
j += 1;
}
if saw_row && j < output.len() && output[j] == b';' {
j += 1;
let mut col: u16 = 0;
let mut saw_col = false;
while j < output.len() && output[j].is_ascii_digit() {
saw_col = true;
col = col
.saturating_mul(10)
.saturating_add(u16::from(output[j] - b'0'));
j += 1;
}
if saw_col && j < output.len() && output[j] == b'H' {
results.push((row, col));
}
}
}
i += 1;
}
results
}
fn parse_decstbm(output: &[u8]) -> Vec<(u16, u16)> {
let mut results = Vec::new();
let mut i = 0;
while i + 2 < output.len() {
if output[i] == 0x1b && output[i + 1] == b'[' {
let mut j = i + 2;
let mut top: u16 = 0;
let mut saw_top = false;
while j < output.len() && output[j].is_ascii_digit() {
saw_top = true;
top = top
.saturating_mul(10)
.saturating_add(u16::from(output[j] - b'0'));
j += 1;
}
if saw_top && j < output.len() && output[j] == b';' {
j += 1;
let mut bottom: u16 = 0;
let mut saw_bottom = false;
while j < output.len() && output[j].is_ascii_digit() {
saw_bottom = true;
bottom = bottom
.saturating_mul(10)
.saturating_add(u16::from(output[j] - b'0'));
j += 1;
}
if saw_bottom && j < output.len() && output[j] == b'r' {
results.push((top, bottom));
}
}
}
i += 1;
}
results
}
fn compute_output_checksum(output: &[u8]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
output.hash(&mut hasher);
format!("crc:{:016x}", hasher.finish())
}
fn is_log_enabled() -> bool {
std::env::var("INLINE_REFLOW_LOG").is_ok()
}
fn log_jsonl(event: &str, fields: &[(&str, &str)]) {
if !is_log_enabled() {
return;
}
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let kvs: Vec<String> = fields
.iter()
.map(|(k, v)| format!("\"{}\":\"{}\"", k, v))
.collect();
eprintln!(
"{{\"event\":\"{}\",\"ts_ms\":{},{}}}",
event,
ts,
kvs.join(",")
);
}
fn make_buffer_with_pattern(width: u16, height: u16, ch: char) -> Buffer {
let mut buf = Buffer::new(width, height);
for y in 0..height {
for x in 0..width {
buf.set_raw(x, y, Cell::from_char(ch));
}
}
buf
}
#[test]
fn inline_reflow_mode1_inline_fixed_height() {
log_jsonl("start", &[("test", "mode1_inline_fixed_height")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::Inline { ui_height: 10 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
assert_eq!(
w.ui_height(),
10,
"Fixed inline should report exact ui_height"
);
assert_eq!(
w.render_height_hint(),
10,
"Fixed inline render_height_hint = ui_height"
);
assert_eq!(w.screen_mode(), ScreenMode::Inline { ui_height: 10 });
log_jsonl("pass", &[("test", "mode1_inline_fixed_height")]);
}
#[test]
fn inline_reflow_mode1_altscreen_uses_terminal_height() {
log_jsonl("start", &[("test", "mode1_altscreen")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::AltScreen,
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
assert_eq!(
w.ui_height(),
24,
"AltScreen should use full terminal height"
);
assert_eq!(w.render_height_hint(), 24);
log_jsonl("pass", &[("test", "mode1_altscreen")]);
}
#[test]
fn inline_reflow_mode1_inline_auto_defaults_to_min() {
log_jsonl("start", &[("test", "mode1_inline_auto_defaults")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 4,
max_height: 12,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
assert_eq!(
w.ui_height(),
4,
"InlineAuto should default to min before measurement"
);
assert_eq!(
w.render_height_hint(),
12,
"render_height_hint should be max for measurement"
);
log_jsonl("pass", &[("test", "mode1_inline_auto_defaults")]);
}
#[test]
fn inline_reflow_auto1_clamp_between_min_max() {
log_jsonl("start", &[("test", "auto1_clamp")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 8,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
w.set_auto_ui_height(1);
assert_eq!(w.ui_height(), 3, "Below min should clamp to min");
assert_eq!(w.auto_ui_height(), Some(3));
w.set_auto_ui_height(20);
assert_eq!(w.ui_height(), 8, "Above max should clamp to max");
assert_eq!(w.auto_ui_height(), Some(8));
w.set_auto_ui_height(3);
assert_eq!(w.ui_height(), 3);
w.set_auto_ui_height(8);
assert_eq!(w.ui_height(), 8);
w.set_auto_ui_height(6);
assert_eq!(w.ui_height(), 6);
log_jsonl("pass", &[("test", "auto1_clamp")]);
}
#[test]
fn inline_reflow_auto1_clamp_to_terminal_height() {
log_jsonl("start", &[("test", "auto1_clamp_term")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 50,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 10);
w.set_auto_ui_height(50);
assert!(
w.ui_height() <= 10,
"Auto height {} must not exceed terminal height 10",
w.ui_height()
);
log_jsonl("pass", &[("test", "auto1_clamp_term")]);
}
#[test]
fn inline_reflow_auto1_sanitize_min_gt_max() {
log_jsonl("start", &[("test", "auto1_sanitize")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 10,
max_height: 5, },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let bounds = w.inline_auto_bounds();
assert!(bounds.is_some());
let (min, max) = bounds.unwrap();
assert!(min <= max, "Sanitized min {} should be <= max {}", min, max);
log_jsonl("pass", &[("test", "auto1_sanitize")]);
}
#[test]
fn inline_reflow_auto2_set_size_invalidates_cache() {
log_jsonl("start", &[("test", "auto2_invalidation")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 8,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
w.set_auto_ui_height(6);
assert_eq!(w.auto_ui_height(), Some(6));
w.set_size(100, 30);
assert_eq!(
w.auto_ui_height(),
None,
"set_size must clear auto_ui_height cache"
);
assert_eq!(w.render_height_hint(), 8);
log_jsonl("pass", &[("test", "auto2_invalidation")]);
}
#[test]
fn inline_reflow_auto2_clear_auto_height_explicit() {
log_jsonl("start", &[("test", "auto2_explicit_clear")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 8,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
w.set_auto_ui_height(5);
assert_eq!(w.auto_ui_height(), Some(5));
w.clear_auto_ui_height();
assert_eq!(w.auto_ui_height(), None);
assert_eq!(w.render_height_hint(), 8, "After clear, hint should be max");
log_jsonl("pass", &[("test", "auto2_explicit_clear")]);
}
#[test]
fn inline_reflow_auto2_fixed_mode_ignores_auto_api() {
log_jsonl("start", &[("test", "auto2_fixed_ignores")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::Inline { ui_height: 10 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
w.set_auto_ui_height(5);
assert_eq!(
w.auto_ui_height(),
None,
"Fixed mode should not have auto_ui_height"
);
assert_eq!(w.ui_height(), 10, "Fixed mode height should not change");
assert!(
w.inline_auto_bounds().is_none(),
"Fixed mode has no auto bounds"
);
log_jsonl("pass", &[("test", "auto2_fixed_ignores")]);
}
#[test]
fn inline_reflow_strat1_selection_matrix() {
log_jsonl("start", &[("test", "strat1_matrix")]);
let cases: Vec<(TerminalCapabilities, InlineStrategy, &str)> = vec![
(basic_caps(), InlineStrategy::OverlayRedraw, "basic→overlay"),
(modern_caps(), InlineStrategy::ScrollRegion, "modern→scroll"),
(hybrid_caps(), InlineStrategy::Hybrid, "hybrid_caps→hybrid"),
(mux_caps(), InlineStrategy::OverlayRedraw, "mux→overlay"),
(dumb_caps(), InlineStrategy::OverlayRedraw, "dumb→overlay"),
];
for (caps, expected, label) in &cases {
let strategy = InlineStrategy::select(caps);
assert_eq!(
strategy, *expected,
"Strategy selection mismatch for {}: got {:?}, expected {:?}",
label, strategy, expected
);
log_jsonl("case", &[("label", label), ("result", "pass")]);
}
log_jsonl("pass", &[("test", "strat1_matrix")]);
}
#[test]
fn inline_reflow_strat1_writer_inherits_strategy() {
log_jsonl("start", &[("test", "strat1_writer_inherits")]);
let w = TerminalWriter::new(
Vec::new(),
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
assert_eq!(w.inline_strategy(), InlineStrategy::ScrollRegion);
let w = TerminalWriter::new(
Vec::new(),
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
mux_caps(),
);
assert_eq!(w.inline_strategy(), InlineStrategy::OverlayRedraw);
log_jsonl("pass", &[("test", "strat1_writer_inherits")]);
}
#[test]
fn inline_reflow_strat2_overlay_no_scroll_region() {
log_jsonl("start", &[("test", "strat2_overlay_no_sr")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
}
let regions = parse_decstbm(&output);
assert!(
regions.is_empty(),
"Overlay strategy should emit no DECSTBM sequences, got {:?}",
regions
);
log_jsonl("pass", &[("test", "strat2_overlay_no_sr")]);
}
#[test]
fn inline_reflow_strat2_scroll_region_emits_decstbm() {
log_jsonl("start", &[("test", "strat2_sr_decstbm")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
}
let regions = parse_decstbm(&output);
assert!(
!regions.is_empty(),
"ScrollRegion strategy should emit DECSTBM"
);
assert!(
regions
.iter()
.any(|&(top, bottom)| top == 1 && bottom == 19),
"Expected DECSTBM(1,19), got {:?}",
regions
);
log_jsonl("pass", &[("test", "strat2_sr_decstbm")]);
}
#[test]
fn inline_reflow_strat2_sync_output_wraps_present() {
log_jsonl("start", &[("test", "strat2_sync_wraps")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
}
assert!(contains(&output, SYNC_BEGIN), "Should have sync begin");
assert!(contains(&output, SYNC_END), "Should have sync end");
let begin_pos = output
.windows(SYNC_BEGIN.len())
.position(|w| w == SYNC_BEGIN)
.unwrap();
let end_pos = output
.windows(SYNC_END.len())
.position(|w| w == SYNC_END)
.unwrap();
assert!(begin_pos < end_pos, "Sync begin must precede sync end");
log_jsonl("pass", &[("test", "strat2_sync_wraps")]);
}
#[test]
fn inline_reflow_strat2_no_sync_without_capability() {
log_jsonl("start", &[("test", "strat2_no_sync")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(), );
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
}
assert!(
!contains(&output, SYNC_BEGIN),
"No sync begin without capability"
);
assert!(
!contains(&output, SYNC_END),
"No sync end without capability"
);
log_jsonl("pass", &[("test", "strat2_no_sync")]);
}
#[test]
fn inline_reflow_cursor1_save_restore_pair() {
log_jsonl("start", &[("test", "cursor1_pair")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
}
let saves = count_occurrences(&output, CURSOR_SAVE);
let restores = count_occurrences(&output, CURSOR_RESTORE);
assert!(saves >= 1, "Should have at least 1 cursor save");
assert!(restores >= 1, "Should have at least 1 cursor restore");
log_jsonl("pass", &[("test", "cursor1_pair")]);
}
#[test]
fn inline_reflow_cursor1_multiple_presents() {
log_jsonl("start", &[("test", "cursor1_multiple")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
w.present_ui(&buf, None, false).unwrap();
w.present_ui(&buf, None, false).unwrap();
}
let saves = count_occurrences(&output, CURSOR_SAVE);
assert_eq!(saves, 3, "Three presents should produce 3 cursor saves");
log_jsonl("pass", &[("test", "cursor1_multiple")]);
}
#[test]
fn inline_reflow_cursor1_save_before_restore() {
log_jsonl("start", &[("test", "cursor1_order")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
}
let save_pos = output
.windows(CURSOR_SAVE.len())
.position(|w| w == CURSOR_SAVE);
let restore_pos = output
.windows(CURSOR_RESTORE.len())
.position(|w| w == CURSOR_RESTORE);
assert!(save_pos.is_some() && restore_pos.is_some());
assert!(
save_pos.unwrap() < restore_pos.unwrap(),
"Cursor save must precede restore"
);
log_jsonl("pass", &[("test", "cursor1_order")]);
}
#[test]
fn inline_reflow_cursor1_altscreen_no_cursor_gymnastics() {
log_jsonl("start", &[("test", "cursor1_altscreen")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::AltScreen,
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 10);
w.present_ui(&buf, None, false).unwrap();
}
let cups = parse_cup_sequences(&output);
log_jsonl("pass", &[("test", "cursor1_altscreen")]);
let _ = cups;
}
#[test]
fn inline_reflow_ghost1_shrink_clears_stale_rows() {
log_jsonl("start", &[("test", "ghost1_shrink")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::InlineAuto {
min_height: 1,
max_height: 8,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 8, '#');
w.set_auto_ui_height(8);
w.present_ui(&buf, None, false).unwrap();
w.set_auto_ui_height(3);
let buf2 = make_buffer_with_pattern(10, 3, '.');
w.present_ui(&buf2, None, false).unwrap();
}
let second_save = find_nth(&output, CURSOR_SAVE, 2).expect("expected second cursor save");
let after_second_save = &output[second_save..];
let restore_idx = after_second_save
.windows(CURSOR_RESTORE.len())
.position(|w| w == CURSOR_RESTORE)
.expect("expected cursor restore after second save");
let shrink_segment = &after_second_save[..restore_idx];
let erase_count = count_occurrences(shrink_segment, ERASE_LINE);
assert!(
erase_count >= 3,
"Shrink from 8→3 should clear at least 3 rows, got {} erases",
erase_count
);
log_jsonl("pass", &[("test", "ghost1_shrink")]);
}
#[test]
fn inline_reflow_ghost1_no_full_screen_clear_in_inline() {
log_jsonl("start", &[("test", "ghost1_no_full_clear")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 5);
w.present_ui(&buf, None, false).unwrap();
w.present_ui(&buf, None, false).unwrap();
}
assert!(
!contains(&output, FULL_CLEAR),
"Inline mode must never use full screen clear (ED2)"
);
log_jsonl("pass", &[("test", "ghost1_no_full_clear")]);
}
#[test]
fn inline_reflow_ghost1_buffer_shorter_than_ui_height() {
log_jsonl("start", &[("test", "ghost1_short_buffer")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 10 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 20);
let buf_full = make_buffer_with_pattern(10, 10, 'X');
w.present_ui(&buf_full, None, false).unwrap();
let buf_short = make_buffer_with_pattern(10, 3, '.');
w.present_ui(&buf_short, None, false).unwrap();
}
let second_save = find_nth(&output, CURSOR_SAVE, 2).expect("expected second cursor save");
let after_second_save = &output[second_save..];
let restore_idx = after_second_save
.windows(CURSOR_RESTORE.len())
.position(|w| w == CURSOR_RESTORE)
.expect("expected cursor restore after second save");
let short_segment = &after_second_save[..restore_idx];
let erase_count = count_occurrences(short_segment, ERASE_LINE);
assert!(
erase_count >= 7,
"Short buffer (3) in 10-row UI should clear 7 stale rows, got {}",
erase_count
);
log_jsonl("pass", &[("test", "ghost1_short_buffer")]);
}
#[test]
fn inline_reflow_scroll1_activate_on_present() {
log_jsonl("start", &[("test", "scroll1_activate")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
assert!(!w.scroll_region_active(), "Not active before present");
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
assert!(
w.scroll_region_active(),
"Active after present with scroll strategy"
);
log_jsonl("pass", &[("test", "scroll1_activate")]);
}
#[test]
fn inline_reflow_scroll1_resize_deactivates() {
log_jsonl("start", &[("test", "scroll1_resize_deactivates")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
assert!(w.scroll_region_active());
w.set_size(100, 30);
assert!(
!w.scroll_region_active(),
"Resize must deactivate scroll region"
);
log_jsonl("pass", &[("test", "scroll1_resize_deactivates")]);
}
#[test]
fn inline_reflow_scroll1_cleanup_resets() {
log_jsonl("start", &[("test", "scroll1_cleanup")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
}
assert!(
contains(&output, RESET_SCROLL_REGION),
"Cleanup must reset scroll region"
);
log_jsonl("pass", &[("test", "scroll1_cleanup")]);
}
#[test]
fn inline_reflow_scroll1_top_anchor_region() {
log_jsonl("start", &[("test", "scroll1_top_anchor")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Top,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
}
let regions = parse_decstbm(&output);
assert!(
regions
.iter()
.any(|&(top, bottom)| top == 6 && bottom == 24),
"Top anchor should set DECSTBM(6,24), got {:?}",
regions
);
log_jsonl("pass", &[("test", "scroll1_top_anchor")]);
}
#[test]
fn inline_reflow_scroll1_reactivate_after_resize() {
log_jsonl("start", &[("test", "scroll1_reactivate")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
assert!(w.scroll_region_active());
w.set_size(80, 40);
assert!(!w.scroll_region_active());
let buf2 = Buffer::new(80, 5);
w.present_ui(&buf2, None, false).unwrap();
assert!(
w.scroll_region_active(),
"Scroll region should reactivate after resize + present"
);
}
let regions = parse_decstbm(&output);
assert!(
regions
.iter()
.any(|&(top, bottom)| top == 1 && bottom == 35),
"After resize to 40, should set DECSTBM(1,35), got {:?}",
regions
);
log_jsonl("pass", &[("test", "scroll1_reactivate")]);
}
#[test]
fn inline_reflow_idem1_same_buffer_twice() {
log_jsonl("start", &[("test", "idem1_same_twice")]);
let mut output1 = Vec::new();
let mut output2 = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output1,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'A');
w.present_ui(&buf, None, false).unwrap();
}
{
let mut w = TerminalWriter::new(
&mut output2,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'A');
w.present_ui(&buf, None, false).unwrap();
}
let chk1 = compute_output_checksum(&output1);
let chk2 = compute_output_checksum(&output2);
assert_eq!(
chk1, chk2,
"Identical buffers from identical state should produce identical output"
);
log_jsonl("pass", &[("test", "idem1_same_twice"), ("checksum", &chk1)]);
}
#[test]
fn inline_reflow_idem1_diff_minimal_on_repeat() {
log_jsonl("start", &[("test", "idem1_diff_minimal")]);
let mut output_single = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output_single,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'A');
w.present_ui(&buf, None, false).unwrap();
}
let mut output_double = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output_double,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'A');
w.present_ui(&buf, None, false).unwrap();
w.present_ui(&buf, None, false).unwrap();
}
let first_len = output_single.len();
let second_len = output_double.len().saturating_sub(first_len);
assert!(
second_len < first_len,
"Repeat present ({} bytes) should be smaller than initial ({} bytes)",
second_len,
first_len
);
log_jsonl("pass", &[("test", "idem1_diff_minimal")]);
}
#[test]
fn inline_reflow_mono1_larger_max_never_smaller_hint() {
log_jsonl("start", &[("test", "mono1_monotone")]);
let max_values = [4, 8, 12, 16, 20, 24];
let mut prev_hint = 0u16;
for &max_h in &max_values {
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: max_h,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let hint = w.render_height_hint();
assert!(
hint >= prev_hint,
"Monotone violation: max_height={} → hint={} < prev_hint={}",
max_h,
hint,
prev_hint
);
prev_hint = hint;
log_jsonl(
"case",
&[
("max_height", &max_h.to_string()),
("hint", &hint.to_string()),
],
);
}
log_jsonl("pass", &[("test", "mono1_monotone")]);
}
#[test]
fn inline_reflow_mono1_larger_cached_height_larger_effective() {
log_jsonl("start", &[("test", "mono1_cached_monotone")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 12,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let heights = [3, 5, 7, 9, 11, 12];
let mut prev_effective = 0u16;
for &h in &heights {
w.set_auto_ui_height(h);
let effective = w.ui_height();
assert!(
effective >= prev_effective,
"Monotone violation: set {} → effective {} < prev {}",
h,
effective,
prev_effective
);
prev_effective = effective;
}
log_jsonl("pass", &[("test", "mono1_cached_monotone")]);
}
#[test]
fn inline_reflow_anchor_bottom_ui_at_bottom() {
log_jsonl("start", &[("test", "anchor_bottom")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 5, 'B');
w.present_ui(&buf, None, false).unwrap();
}
let cups = parse_cup_sequences(&output);
assert!(
cups.iter().any(|&(row, _)| row >= 20),
"Bottom-anchored UI should position at row >= 20, got {:?}",
cups
);
log_jsonl("pass", &[("test", "anchor_bottom")]);
}
#[test]
fn inline_reflow_anchor_top_ui_at_top() {
log_jsonl("start", &[("test", "anchor_top")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Top,
basic_caps(),
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 5, 'T');
w.present_ui(&buf, None, false).unwrap();
}
let cups = parse_cup_sequences(&output);
assert!(
cups.iter().any(|&(row, _)| row == 1),
"Top-anchored UI should position at row 1, got {:?}",
cups
);
log_jsonl("pass", &[("test", "anchor_top")]);
}
#[test]
fn inline_reflow_log_write_does_not_corrupt_ui_region() {
log_jsonl("start", &[("test", "log_no_corrupt")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 5, 'U');
w.present_ui(&buf, None, false).unwrap();
w.write_log("log line 1\n").unwrap();
w.write_log("log line 2\n").unwrap();
w.present_ui(&buf, None, false).unwrap();
}
let text = String::from_utf8_lossy(&output);
assert!(text.contains("log line 1"));
assert!(text.contains("log line 2"));
log_jsonl("pass", &[("test", "log_no_corrupt")]);
}
#[test]
fn inline_reflow_log_write_altscreen_silent() {
log_jsonl("start", &[("test", "log_altscreen_silent")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::AltScreen,
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
w.write_log("should not appear\n").unwrap();
}
let text = String::from_utf8_lossy(&output);
assert!(
!text.contains("should not appear"),
"AltScreen should silently drop log writes"
);
log_jsonl("pass", &[("test", "log_altscreen_silent")]);
}
#[test]
fn inline_reflow_cleanup_resets_all_state() {
log_jsonl("start", &[("test", "cleanup_reset_all")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
}
assert!(
contains(&output, RESET_SCROLL_REGION),
"Should reset scroll region"
);
assert!(contains(&output, b"\x1b[0m"), "Should reset style");
assert!(contains(&output, b"\x1b[?25h"), "Should show cursor");
log_jsonl("pass", &[("test", "cleanup_reset_all")]);
}
#[test]
fn inline_reflow_cleanup_ends_sync_block() {
log_jsonl("start", &[("test", "cleanup_sync")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(80, 24);
let buf = Buffer::new(80, 5);
w.present_ui(&buf, None, false).unwrap();
}
assert!(
contains(&output, SYNC_END),
"Cleanup should ensure sync block is ended"
);
log_jsonl("pass", &[("test", "cleanup_sync")]);
}
#[test]
fn inline_reflow_golden_fixed_10x5() {
log_jsonl("start", &[("test", "golden_fixed_10x5")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'G');
w.present_ui(&buf, None, false).unwrap();
}
let checksum = compute_output_checksum(&output);
log_jsonl(
"golden",
&[
("test", "golden_fixed_10x5"),
("checksum", &checksum),
("output_len", &output.len().to_string()),
],
);
let mut output2 = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output2,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = make_buffer_with_pattern(10, 5, 'G');
w.present_ui(&buf, None, false).unwrap();
}
let checksum2 = compute_output_checksum(&output2);
assert_eq!(checksum, checksum2, "Golden output must be deterministic");
log_jsonl("pass", &[("test", "golden_fixed_10x5")]);
}
#[test]
fn inline_reflow_golden_80x24_bottom() {
log_jsonl("start", &[("test", "golden_80x24_bottom")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 10 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 10, '#');
w.present_ui(&buf, None, false).unwrap();
}
let chk = compute_output_checksum(&output);
let mut out2 = Vec::new();
{
let mut w = TerminalWriter::new(
&mut out2,
ScreenMode::Inline { ui_height: 10 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 10, '#');
w.present_ui(&buf, None, false).unwrap();
}
assert_eq!(chk, compute_output_checksum(&out2));
log_jsonl(
"pass",
&[("test", "golden_80x24_bottom"), ("checksum", &chk)],
);
}
#[test]
fn inline_reflow_golden_strategies_differ() {
log_jsonl("start", &[("test", "golden_strategies_differ")]);
let caps_variants: Vec<(TerminalCapabilities, &str)> = vec![
(basic_caps(), "overlay"),
(modern_caps(), "scroll_region"),
(hybrid_caps(), "hybrid"),
];
let mut checksums = BTreeMap::new();
for (caps, label) in &caps_variants {
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 5 },
UiAnchor::Bottom,
*caps,
);
w.set_size(80, 24);
let buf = make_buffer_with_pattern(80, 5, 'S');
w.present_ui(&buf, None, false).unwrap();
}
let chk = compute_output_checksum(&output);
checksums.insert(*label, chk);
}
assert_ne!(
checksums["overlay"], checksums["scroll_region"],
"Overlay and scroll region strategies should produce different output"
);
log_jsonl("pass", &[("test", "golden_strategies_differ")]);
}
#[test]
fn inline_reflow_edge_zero_height_ui() {
log_jsonl("start", &[("test", "edge_zero_height")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 0 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 10);
let buf = Buffer::new(10, 1);
w.present_ui(&buf, None, false).unwrap();
}
assert!(!contains(&output, FULL_CLEAR));
log_jsonl("pass", &[("test", "edge_zero_height")]);
}
#[test]
fn inline_reflow_edge_ui_larger_than_terminal() {
log_jsonl("start", &[("test", "edge_ui_larger")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 100 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 5);
let buf = make_buffer_with_pattern(10, 100, 'X');
w.present_ui(&buf, None, false).unwrap();
}
let cups = parse_cup_sequences(&output);
for &(row, _) in &cups {
assert!(row <= 5, "Cursor row {} exceeds terminal height 5", row);
}
log_jsonl("pass", &[("test", "edge_ui_larger")]);
}
#[test]
fn inline_reflow_edge_single_row_terminal() {
log_jsonl("start", &[("test", "edge_single_row")]);
let mut output = Vec::new();
{
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::Inline { ui_height: 1 },
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(10, 1);
let buf = make_buffer_with_pattern(10, 1, '-');
w.present_ui(&buf, None, false).unwrap();
}
log_jsonl("pass", &[("test", "edge_single_row")]);
}
#[test]
fn inline_reflow_edge_rapid_resize_sequence() {
log_jsonl("start", &[("test", "edge_rapid_resize")]);
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: 3,
max_height: 20,
},
UiAnchor::Bottom,
modern_caps(),
);
let sizes: Vec<(u16, u16)> = vec![(80, 24), (120, 40), (40, 10), (200, 60), (80, 24), (60, 15)];
for &(w_size, h_size) in &sizes {
w.set_size(w_size, h_size);
assert!(
w.auto_ui_height().is_none(),
"Auto height should be cleared after resize to {}x{}",
w_size,
h_size
);
let buf = make_buffer_with_pattern(w_size, w.render_height_hint(), '.');
w.present_ui(&buf, None, false).unwrap();
}
log_jsonl("pass", &[("test", "edge_rapid_resize")]);
}
#[test]
fn inline_reflow_large_screen_burst_logs_inline_and_altscreen() {
log_jsonl("start", &[("test", "large_screen_burst_logs")]);
let mut inline_output = Vec::new();
{
let mut inline = TerminalWriter::new(
&mut inline_output,
ScreenMode::Inline { ui_height: 12 },
UiAnchor::Bottom,
modern_caps(),
);
let inline_sizes = [(240, 80), (200, 60), (40, 12), (240, 80)];
for (idx, &(w_size, h_size)) in inline_sizes.iter().enumerate() {
inline.set_size(w_size, h_size);
let buf = make_buffer_with_pattern(w_size, inline.render_height_hint(), '#');
inline.present_ui(&buf, None, false).unwrap();
if idx == 0 || idx == 2 {
for i in 0..25 {
inline.write_log(&format!("burst {idx}-{i}\n")).unwrap();
}
}
}
}
assert!(
!contains(&inline_output, FULL_CLEAR),
"Inline mode must not emit full screen clear (ED2)"
);
let mut alt_output = Vec::new();
let mut alt = TerminalWriter::new(
&mut alt_output,
ScreenMode::AltScreen,
UiAnchor::Bottom,
modern_caps(),
);
let alt_sizes = [(240, 80), (200, 60), (40, 12)];
for &(w_size, h_size) in &alt_sizes {
alt.set_size(w_size, h_size);
let buf = make_buffer_with_pattern(w_size, h_size, '@');
alt.present_ui(&buf, None, false).unwrap();
}
log_jsonl("pass", &[("test", "large_screen_burst_logs")]);
}
#[test]
fn inline_reflow_inline_auto_remeasure_churn_large() {
log_jsonl("start", &[("test", "inline_auto_churn_large")]);
let mut output = Vec::new();
let mut w = TerminalWriter::new(
&mut output,
ScreenMode::InlineAuto {
min_height: 4,
max_height: 20,
},
UiAnchor::Bottom,
modern_caps(),
);
w.set_size(240, 80);
let heights = [4u16, 8, 12, 18, 6, 20, 10];
for (idx, height) in heights.iter().enumerate() {
w.set_auto_ui_height(*height);
let effective = w.auto_ui_height().unwrap_or(0);
assert!(
(4..=20).contains(&effective),
"auto ui height {effective} out of bounds"
);
let ch = if idx % 2 == 0 { 'U' } else { 'V' };
let buf = make_buffer_with_pattern(240, w.render_height_hint(), ch);
w.present_ui(&buf, None, false).unwrap();
}
log_jsonl("pass", &[("test", "inline_auto_churn_large")]);
}
#[test]
fn inline_reflow_property_hint_geq_min() {
log_jsonl("start", &[("test", "property_hint_geq_min")]);
let min_values = [1, 3, 5, 10, 15];
let max_values = [5, 10, 15, 20, 24, 30];
let term_heights = [10, 24, 40, 60];
let mut tested = 0u32;
for &min_h in &min_values {
for &max_h in &max_values {
for &term_h in &term_heights {
let output = Vec::new();
let mut w = TerminalWriter::new(
output,
ScreenMode::InlineAuto {
min_height: min_h,
max_height: max_h,
},
UiAnchor::Bottom,
basic_caps(),
);
w.set_size(80, term_h);
let hint = w.render_height_hint();
let bounds = w.inline_auto_bounds().unwrap();
let effective_min = bounds.0;
assert!(
hint >= effective_min,
"render_height_hint {} < effective_min {} for min={}, max={}, term={}",
hint,
effective_min,
min_h,
max_h,
term_h
);
tested += 1;
}
}
}
log_jsonl(
"pass",
&[
("test", "property_hint_geq_min"),
("cases_tested", &tested.to_string()),
],
);
}
#[test]
fn inline_reflow_property_ui_height_leq_term() {
log_jsonl("start", &[("test", "property_ui_leq_term")]);
let term_heights = [5, 10, 15, 24, 40];
let modes: Vec<(ScreenMode, &str)> = vec![
(ScreenMode::Inline { ui_height: 3 }, "inline_3"),
(ScreenMode::Inline { ui_height: 10 }, "inline_10"),
(ScreenMode::Inline { ui_height: 50 }, "inline_50"),
(
ScreenMode::InlineAuto {
min_height: 3,
max_height: 8,
},
"auto_3_8",
),
(
ScreenMode::InlineAuto {
min_height: 1,
max_height: 100,
},
"auto_1_100",
),
(ScreenMode::AltScreen, "altscreen"),
];
let mut tested = 0u32;
for &term_h in &term_heights {
for (mode, label) in &modes {
let output = Vec::new();
let mut w = TerminalWriter::new(output, *mode, UiAnchor::Bottom, basic_caps());
w.set_size(80, term_h);
let effective = w.ui_height();
match mode {
ScreenMode::Inline { .. } => {
}
ScreenMode::InlineAuto { .. } | ScreenMode::AltScreen => {
assert!(
effective <= term_h,
"{}: ui_height {} > term_height {} (should be ≤)",
label,
effective,
term_h
);
}
}
tested += 1;
}
}
log_jsonl(
"pass",
&[
("test", "property_ui_leq_term"),
("cases_tested", &tested.to_string()),
],
);
}
#[test]
fn inline_reflow_suite_summary() {
let invariants = [
("MODE-1", "ScreenMode identity", 3),
("AUTO-1", "InlineAuto clamping", 3),
("AUTO-2", "Cache invalidation", 3),
("STRAT-1", "Strategy selection", 2),
("STRAT-2", "Strategy ANSI contracts", 4),
("CURSOR-1", "Cursor save/restore", 4),
("GHOST-1", "No ghosting on shrink", 3),
("SCROLL-1", "Scroll region lifecycle", 5),
("IDEM-1", "Idempotent present", 2),
("MONO-1", "Monotone height response", 2),
];
let total_tests: usize = invariants.iter().map(|(_, _, n)| n).sum::<usize>()
+ 2 + 2 + 2 + 3 + 4 + 2 + 1;
log_jsonl(
"suite_summary",
&[
("invariants", &invariants.len().to_string()),
("total_tests", &total_tests.to_string()),
],
);
eprintln!("\n=== Inline Reflow Policy Test Suite (bd-1rz0.4.3) ===");
eprintln!("Invariants tested: {}", invariants.len());
for (id, desc, count) in &invariants {
eprintln!(" {}: {} ({} tests)", id, desc, count);
}
eprintln!("Additional: anchors(2), logs(2), cleanup(2), golden(3), edge(4), property(2)");
eprintln!("Total test functions: {}", total_tests);
eprintln!("============================================\n");
}