use super::*;
use test_support::{emacs_editor, key, nano_editor, special, type_chars, vi_editor};
fn normalize_clock(rows: &[String]) -> Vec<String> {
const SHAPE: &str = "[dddd-dd-dd dd:dd:dd]";
let looks_like_stamp = |window: &str| {
window
.chars()
.zip(SHAPE.chars())
.all(|(c, want)| match want {
'd' => c.is_ascii_digit(),
other => c == other,
})
};
rows.iter()
.map(|row| {
let chars: Vec<char> = row.chars().collect();
let mut out = chars.clone();
for start in 0..chars.len().saturating_sub(SHAPE.len() - 1) {
let window: String = chars[start..start + SHAPE.len()].iter().collect();
if looks_like_stamp(&window) {
for (offset, want) in SHAPE.chars().enumerate() {
if want == 'd' {
out[start + offset] = '0';
}
}
}
}
out.into_iter().collect()
})
.collect()
}
#[test]
fn the_clock_normalizer_touches_only_a_timestamp() {
let rows = vec![
"[2026-09-04 20:31:07] vi 1 tab 40%".to_string(),
"no stamp here: 12345".to_string(),
];
let out = normalize_clock(&rows);
assert_eq!(out[0], "[0000-00-00 00:00:00] vi 1 tab 40%");
assert_eq!(out[1], rows[1], "a row without a stamp is untouched");
let a = normalize_clock(&["[2026-09-04 20:31:07] x".to_string()]);
let b = normalize_clock(&["[2026-09-04 20:31:08] x".to_string()]);
assert_eq!(a, b);
let c = normalize_clock(&["[2026-09-04 20:31:08] y".to_string()]);
assert_ne!(b, c);
}
#[test]
fn a_single_tab_frame_is_byte_identical_to_no_bar() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let ed = emacs_editor();
let ta = TextArea::new(vec!["hello".to_string()]);
let render = |tabs: &[crate::tab_bar::TabCell]| -> Vec<String> {
let mut term = Terminal::new(TestBackend::new(40, 5)).unwrap();
term.draw(|f| {
draw(
f,
&ta,
&ed,
Some(1),
RichStatus {
tabs,
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
(0..5)
.map(|y| {
(0..40)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect::<String>()
})
.collect()
};
let none = render(&[]);
let one = render(&[crate::tab_bar::TabCell {
number: 1,
label: "solo".into(),
active: true,
degraded: false,
pending: false,
}]);
let none = normalize_clock(&none);
let one = normalize_clock(&one);
assert_eq!(none, one, "one tab must render exactly like no tabs");
assert!(
!one.iter().any(|r| r.contains("solo")),
"the single tab's label appears nowhere: {one:?}"
);
}
#[test]
fn two_tabs_add_one_row_below_the_clock() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let ed = emacs_editor();
let ta = TextArea::new(vec!["hello".to_string()]);
let cell = |n: usize, l: &str, a: bool| crate::tab_bar::TabCell {
number: n,
label: l.into(),
active: a,
degraded: false,
pending: false,
};
let render = |tabs: &[crate::tab_bar::TabCell]| -> Vec<String> {
let mut term = Terminal::new(TestBackend::new(40, 5)).unwrap();
term.draw(|f| {
draw(
f,
&ta,
&ed,
Some(1),
RichStatus {
tabs,
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
(0..5)
.map(|y| {
(0..40)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect::<String>()
})
.collect()
};
let none = render(&[]);
let two = render(&[cell(1, "build", true), cell(2, "deploy", false)]);
assert!(
two[4].contains("1:build") && two[4].contains("2:deploy"),
"the bar is the bottom-most row: {:?}",
two[4]
);
assert!(
two[3].contains("emacs"),
"the clock sits directly above the bar: {:?}",
two[3]
);
assert!(
none[4].contains("emacs"),
"with one tab the clock is the last row: {:?}",
none[4]
);
assert!(
!none.iter().any(|row| row.contains("1:build")),
"sanity: the no-tab frame has no bar"
);
}
#[test]
fn vi_ex_command_renders_on_a_bottom_row_when_multiline() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut ed = vi_editor();
let mut ta = TextArea::new(vec!["hello".to_string(), "world".to_string()]);
ed.input(special(KeyCode::Esc), &mut ta); ed.input(key(':'), &mut ta); type_chars(&mut ed, &mut ta, "wq");
assert!(
ex_bottom_line(&ed, &ta).is_some(),
"multi-line ex → bottom row"
);
let mut term = Terminal::new(TestBackend::new(40, 4)).unwrap();
term.draw(|f| draw(f, &ta, &ed, Some(1), RichStatus::default()))
.unwrap();
let buf = term.backend().buffer();
let row = |y: u16| -> String {
(0..40)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect::<String>()
};
assert!(
row(2).starts_with(":wq"),
"command on the input region's bottom row: {:?}",
row(2)
);
assert!(
row(1).contains("hello"),
"message renders below the header: {:?}",
row(1)
);
assert!(
!row(0).contains(":wq") && !row(1).contains(":wq"),
"command must NOT be glued to the input rows"
);
}
#[test]
fn vi_ex_command_stays_inline_on_a_single_line() {
let mut ed = vi_editor();
let mut ta = TextArea::new(vec!["hi".to_string()]);
ed.input(special(KeyCode::Esc), &mut ta);
ed.input(key(':'), &mut ta);
type_chars(&mut ed, &mut ta, "wq");
assert!(
ex_bottom_line(&ed, &ta).is_none(),
"single-line stays inline"
);
let line = prompt_line(&ed, true);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains(":wq"), "single-line ex is inline: {text:?}");
}
#[test]
fn vi_ex_command_bottom_row_in_wide_gutter_mode() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut ed = vi_editor();
let mut ta = TextArea::new(vec!["hello".to_string(), "world".to_string()]);
ed.input(special(KeyCode::Esc), &mut ta);
ed.input(key(':'), &mut ta);
type_chars(&mut ed, &mut ta, "wq");
let mut term = Terminal::new(TestBackend::new(80, 4)).unwrap();
term.draw(|f| draw(f, &ta, &ed, Some(25), RichStatus::default()))
.unwrap(); let buf = term.backend().buffer();
let last: String = (0..80)
.map(|x| buf.cell((x, 2)).unwrap().symbol().to_string())
.collect();
assert!(
last.starts_with(":wq"),
"command on the input region's bottom row (wide gutter): {last:?}"
);
}
#[test]
fn slash_palette_renders_above_the_input_row() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut palette = PaletteState::from_corpus();
palette.on_buffer_change("", "/");
palette.on_buffer_change("/", "/model");
let rows = palette.viewport_rows(8);
assert!(rows >= 2, "the /model filter keeps several corpus entries");
palette.set_viewport(rows);
let editor = nano_editor();
let ta = TextArea::new(vec!["/model".to_string()]);
let h = 1 + rows as u16 + 1; let mut term = Terminal::new(TestBackend::new(100, h)).unwrap();
term.draw(|f| {
draw(
f,
&ta,
&editor,
Some(1),
RichStatus {
palette: Some(&palette),
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
let row = |y: u16| -> String {
(0..100)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect()
};
assert!(
row(1).starts_with("❯ /models "),
"highlight on the first match: {:?}",
row(1)
);
assert!(
row(1).contains("list the models"),
"description rides beside the command: {:?}",
row(1)
);
assert!(
row(h - 2).contains("❯ /model"),
"input row intact below the palette: {:?}",
row(h - 2)
);
assert!(
row(h - 1).contains("nano"),
"and the footer is the last row: {:?}",
row(h - 1)
);
}
#[test]
fn overhang_prompt_is_inline_with_one_space_hanging_continuation() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let editor = vi_editor();
let ta = TextArea::new(vec!["this".to_string(), "more".to_string()]);
let mut term = Terminal::new(TestBackend::new(80, 4)).unwrap();
term.draw(|f| {
draw(
f,
&ta,
&editor,
Some(1),
RichStatus {
model: "m",
endpoint: "http://e:1",
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
let row = |y: u16| -> String {
(0..80)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect::<String>()
};
assert!(
row(3).contains("vi --INSERT--") && row(3).contains("m @ http://e:1"),
"footer row carries mode + model @ endpoint: {:?}",
row(3)
);
assert!(
row(1).contains("❯ this"),
"first input line rides on the prompt row: {:?}",
row(1)
);
assert!(
row(2).starts_with(" more"),
"continuation is 1-space hang-indented: {:?}",
row(2)
);
}
#[test]
fn a_running_job_leads_the_layout_rather_than_shifting_the_input() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let editor = vi_editor();
let textarea = TextArea::default();
let job = BackgroundJob::start("indexing repository");
let mut term = Terminal::new(TestBackend::new(80, 4)).unwrap();
term.draw(|f| {
draw(
f,
&textarea,
&editor,
Some(1),
RichStatus {
model: "m",
endpoint: "http://e:1",
background_jobs: std::slice::from_ref(&job),
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
let row = |y: u16| -> String {
(0..80)
.map(|x| buf.cell((x, y)).unwrap().symbol().to_string())
.collect::<String>()
};
assert!(
row(0).contains("background") && row(0).contains("indexing repository"),
"the live job leads: {:?}",
row(0)
);
assert!(
row(2).contains('\u{276f}'),
"the prompt sits below the activity row and the header: {:?}",
row(2)
);
}
#[test]
fn completed_background_job_has_no_indicator_row() {
let first = BackgroundJob::start("indexing repository");
let second = BackgroundJob::start("warming symbols");
let text = |jobs: &[BackgroundJob]| {
background_line(jobs, 0).map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
})
};
let both = text(&[first.clone(), second.clone()]).unwrap();
assert!(both.contains("background (2)"), "{both}");
assert!(both.contains(first.label()) && both.contains(second.label()));
first.finish();
let one = text(&[first, second.clone()]).unwrap();
assert!(!one.contains("background (2)"), "{one}");
assert!(!one.contains("indexing repository"), "{one}");
assert!(one.contains(second.label()), "{one}");
second.finish();
assert!(text(&[second]).is_none());
}
fn row_text(editor: &Editor) -> String {
prompt_line(editor, true)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
fn footer_text(editor: &Editor, model: &str, endpoint: &str) -> String {
footer_line(editor, model, endpoint, None, true)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
fn header_text(session: &str, headline: &str) -> String {
header_line(session, headline)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
#[test]
fn the_header_carries_identity_and_nothing_else() {
let named = header_text("mesh docking", "");
assert!(named.contains("[mesh docking]"), "{named}");
assert!(header_text("#a1b2c3d4", "").contains("[#a1b2c3d4]"));
assert!(header_text("ephemeral", "").contains("[ephemeral]"));
let with_prose = header_text("mesh docking", "wiring the dock");
assert_eq!(with_prose, "[mesh docking] wiring the dock");
assert_eq!(header_text("", "just prose"), "just prose");
assert_eq!(header_text("solo", ""), "[solo]");
assert_eq!(header_text("", ""), "");
for absent in ["vi", "@", "k/"] {
assert!(
!with_prose.contains(absent),
"`{absent}` belongs to the footer: {with_prose}"
);
}
}
#[test]
fn no_region_draws_a_horizontal_rule() {
let ed = vi_editor();
let rows = [
header_text("session", "headline"),
footer_text(&ed, "model", "http://endpoint"),
];
for row in rows {
for rule in ['\u{2500}', '\u{2501}', '\u{2550}', '_'] {
assert!(
!row.contains(&rule.to_string().repeat(4)),
"a run of `{rule}` is a rule: {row}"
);
}
}
}
#[test]
fn header_shows_context_budget_gauge_when_known() {
let ed = vi_editor();
let text = |g| -> String {
footer_line(&ed, "m", "e", g, true)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
};
assert!(
text(Some((972_000, Some(1_024_000)))).contains("972k/1024k"),
"the gauge shows used/budget once the budget is known"
);
assert!(
!text(None).contains("k/"),
"no gauge until a turn has reported usage"
);
assert!(
!text(Some((100, Some(0)))).contains("k/"),
"a zero budget shows no gauge (no divide-by-zero, no noise)"
);
assert!(
text(Some((31_000, None))).contains("31k/?"),
"no window known → used/?, never a bare hidden gauge"
);
}
#[test]
fn native_status_row_shows_the_insert_indicator() {
let editor = vi_editor();
assert!(row_text(&editor).contains('❯'), "insert indicator");
}
#[test]
fn blocking_modal_recedes_the_chat_chevron_without_changing_its_shape() {
let editor = vi_editor();
let active = prompt_line_with_focus(&editor, true, true);
let inactive = prompt_line_with_focus(&editor, true, false);
assert_eq!(active.spans[0].content, inactive.spans[0].content);
assert_eq!(
active.spans[0].style.fg,
Some(Color::from(newt_core::tty::ACTIVE_INPUT_CT))
);
assert_eq!(inactive.spans[0].style.fg, Some(Color::DarkGray));
}
#[test]
fn no_cell_carries_the_live_accent_while_a_modal_owns_the_keyboard() {
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let editor = vi_editor();
let textarea = TextArea::new(vec!["a draft that survives".to_string()]);
let accent = Color::from(newt_core::tty::ACTIVE_INPUT_CT);
let render = |chat_inactive: bool| {
let (width, height) = (60_u16, 4_u16);
let mut term = Terminal::new(TestBackend::new(width, height)).unwrap();
term.draw(|f| {
draw(
f,
&textarea,
&editor,
Some(1),
RichStatus {
chat_inactive,
..RichStatus::default()
},
);
})
.unwrap();
let buf = term.backend().buffer();
(0..height)
.flat_map(|y| (0..width).map(move |x| (x, y)))
.filter(|&(x, y)| buf.cell((x, y)).unwrap().style().fg == Some(accent))
.count()
};
assert!(
render(false) > 0,
"the mounted chat chevron is accented while it owns the keyboard"
);
assert_eq!(
render(true),
0,
"a modal owns the keyboard: nothing beneath it may still read as live"
);
}
#[test]
fn the_footer_shows_datetime_mode_and_model_endpoint() {
let insert = vi_editor(); let h = footer_text(&insert, "nemotron-3-nano:30b", "http://REDACTED-HOST:11434");
assert!(h.starts_with('['), "datetime stamp: {h:?}");
assert!(h.contains("vi --INSERT--"), "{h:?}");
assert!(
h.contains("nemotron-3-nano:30b @ http://REDACTED-HOST:11434"),
"{h:?}"
);
let mut normal = vi_editor();
let mut ta = TextArea::default();
normal.input(special(KeyCode::Esc), &mut ta);
assert!(footer_text(&normal, "m", "e").contains("vi --NORMAL--"));
assert!(footer_text(&emacs_editor(), "m", "e").contains("emacs"));
assert!(!footer_text(&insert, "", "").contains('@'));
}
#[test]
fn mode_hint_advertises_the_other_editor_modes() {
assert!(vi_editor().mode_hint(false).contains("INSERT"));
assert!(vi_editor().mode_hint(false).contains("/nano"));
assert!(vi_editor().mode_hint(false).contains("/emacs"));
}
#[test]
fn only_an_open_ex_line_shows_the_colon() {
let mut ta = TextArea::default();
assert!(row_text(&vi_editor()).starts_with('❯'));
let mut normal = vi_editor();
normal.input(special(KeyCode::Esc), &mut ta); let row = row_text(&normal);
assert!(
row.starts_with('❯'),
"NORMAL keeps the input chevron: {row:?}"
);
assert!(
!row.trim_start().starts_with(':'),
"NORMAL is not command-line mode: {row:?}"
);
assert!(
footer_text(&normal, "m", "e").contains("vi --NORMAL--"),
"the header carries the mode"
);
assert!(
normal.mode_hint(false).contains("i: insert"),
"the hint says how to get back to typing"
);
let mut ex = vi_editor();
ex.input(special(KeyCode::Esc), &mut ta);
ex.input(key(':'), &mut ta);
ex.input(key('w'), &mut ta);
let exrow = row_text(&ex);
assert!(exrow.contains(":w"), "ex line shows the command: {exrow:?}");
assert!(!exrow.contains('❯'), "the ex line owns the row: {exrow:?}");
ex.input(special(KeyCode::Esc), &mut ta);
let back = row_text(&ex);
assert!(
back.starts_with('❯') && !back.contains(":w"),
"Esc closes the command line, back to the input row: {back:?}"
);
}