Lines
100 %
Functions
Branches
use crate::pane::PaneId;
use super::*;
fn console() -> ConsoleState {
ConsoleState::new()
}
fn type_line(state: &mut ConsoleState, text: &str) {
for c in text.chars() {
state.input.insert_char(c);
#[test]
fn incomplete_form_buffers_and_yields_nothing() {
let mut state = console();
type_line(&mut state, "(list");
assert_eq!(state.take_complete_form(), None);
assert_eq!(state.pending, "(list");
assert!(state.input.buffer().is_empty());
assert!(state.history.is_empty());
fn balanced_form_yields_and_clears_buffer() {
type_line(&mut state, "(+ 1 2)");
let form = state.take_complete_form();
assert_eq!(form.as_deref(), Some("(+ 1 2)"));
assert!(state.pending.is_empty());
fn multiline_form_assembles_then_completes() {
type_line(&mut state, " 1 2)");
assert_eq!(form.as_deref(), Some("(list\n 1 2)"));
fn submitted_form_is_pushed_to_history() {
type_line(&mut state, "(foo)");
let _ = state.take_complete_form();
assert_eq!(state.history, vec!["(foo)".to_string()]);
assert_eq!(state.history_cursor, None);
fn incomplete_form_not_pushed_to_history() {
type_line(&mut state, "(open");
fn history_prev_loads_latest_then_older_entries() {
for form in ["(a)", "(b)", "(c)"] {
type_line(&mut state, form);
state.history_prev();
assert_eq!(state.input.buffer(), "(c)");
assert_eq!(state.input.buffer(), "(b)");
assert_eq!(state.input.buffer(), "(a)");
fn history_next_walks_forward_then_clears_to_fresh_line() {
for form in ["(a)", "(b)"] {
state.history_next();
fn history_navigation_is_noop_without_history() {
fn blank_enter_yields_nothing_and_records_no_history() {
type_line(&mut state, " ");
fn duplicate_consecutive_submissions_both_recorded() {
for _ in 0..2 {
type_line(&mut state, "(a)");
assert_eq!(state.take_complete_form().as_deref(), Some("(a)"));
assert_eq!(state.history, vec!["(a)".to_string(), "(a)".to_string()]);
fn history_next_at_bottom_is_noop() {
fn history_caps_at_max_dropping_oldest() {
for i in 0..(MAX_HISTORY_ENTRIES + 5) {
type_line(&mut state, &format!("(f{i})"));
assert_eq!(state.history.len(), MAX_HISTORY_ENTRIES);
assert_eq!(state.history.first().unwrap(), "(f5)");
assert_eq!(
state.history.last().unwrap(),
&format!("(f{})", MAX_HISTORY_ENTRIES + 4)
);
state.input.buffer(),
format!("(f{})", MAX_HISTORY_ENTRIES + 4)
fn format_result_scalar_number_renders_value() {
assert_eq!(format_result("(:id 1 :value 2)"), vec!["2".to_string()]);
fn format_result_scalar_string_renders_value() {
format_result(r#"(:id 1 :value "hello")"#),
vec!["hello".to_string()]
fn format_result_list_response_aligned_table() {
// A list of two plists: each entity has :name and :symbol fields
let envelope =
r#"(:id 1 :value "((:name \"Alice\" :symbol \"USD\") (:name \"Bob\" :symbol \"EUR\"))")"#;
let lines = format_result(envelope);
assert_eq!(lines.len(), 2, "two rows: {lines:?}");
assert!(lines[0].contains("Alice"), "{lines:?}");
assert!(lines[1].contains("Bob"), "{lines:?}");
// Aligned rows must have equal total length (padding equalises them)
lines[0].len(),
lines[1].len(),
"rows not same length: {lines:?}"
fn format_result_empty_list_string_verbatim() {
// An empty list is not tabulated — verbatim fallback.
let envelope = r#"(:id 1 :value "()")"#;
assert_eq!(format_result(envelope), vec!["()".to_string()]);
fn format_result_non_list_string_verbatim() {
// A string that does not read as a list renders verbatim.
let envelope = r#"(:id 1 :value "hello world")"#;
assert_eq!(format_result(envelope), vec!["hello world".to_string()]);
fn format_result_pair_string_list_tabulated() {
// get-balances returns pair:string — a list whose elements are plist
// strings. It must still tabulate (one line per element), not fall back.
let envelope = r#"(:id 1 :value "(\"100 USD\" \"50 EUR\")")"#;
assert_eq!(lines.len(), 2, "{lines:?}");
assert!(lines.iter().any(|l| l.contains("USD")), "{lines:?}");
assert!(lines.iter().any(|l| l.contains("EUR")), "{lines:?}");
fn format_result_record_list_with_leading_whitespace_tabulated() {
let envelope = r#"(:id 1 :value " ((:name \"Alice\" :symbol \"USD\"))")"#;
assert_eq!(lines.len(), 1, "{lines:?}");
fn format_result_error_envelope_renders_error_prefix() {
let envelope = r#"(:id 1 :error (:code not-found :message "account missing"))"#;
assert_eq!(lines[0], "[error] not-found: account missing");
fn format_result_request_envelope_falls_back_verbatim() {
// Request envelopes are not response frames; parse_wire errors → verbatim
let envelope = "(:id 0 :form (foo))";
assert_eq!(format_result(envelope), vec![envelope.to_string()]);
fn format_result_plain_text_falls_back_verbatim() {
format_result("line one\nline two"),
vec!["line one".to_string(), "line two".to_string()]
fn format_result_worker_stopped_notice_verbatim() {
let notice = "eval worker stopped";
assert_eq!(format_result(notice), vec![notice.to_string()]);
fn align_rows_empty() {
assert!(align_rows(&[]).is_empty());
fn align_rows_single_column() {
let rows = vec![vec!["a".to_string()], vec!["bbb".to_string()]];
let out = align_rows(&rows);
assert_eq!(out, vec!["a", "bbb"]);
fn align_rows_multiple_columns_pads_to_max_width() {
let rows = vec![
vec!["hi".to_string(), "there".to_string()],
vec!["goodbye".to_string(), "ok".to_string()],
];
assert_eq!(out[0], "hi there");
assert_eq!(out[1], "goodbye ok");
fn align_rows_ragged_rows() {
vec!["a".to_string(), "b".to_string(), "c".to_string()],
vec!["longer".to_string()],
assert_eq!(out.len(), 2);
// Second row has only one cell; still renders without panic
assert!(out[1].starts_with("longer"));
fn align_rows_trailing_whitespace_trimmed() {
let rows = vec![vec!["x".to_string()]];
assert_eq!(out[0], "x");
fn format_result_nil_value_renders_nil() {
// NIL scalar value → "NIL"
let lines = format_result("(:id 1 :value NIL)");
assert_eq!(lines, vec!["NIL".to_string()]);
fn scroll_up_increases_offset_clamped_to_len() {
for i in 0..5 {
state.push_scrollback(format!("line {i}"));
state.scroll_up(3);
assert_eq!(state.scroll, 3);
// Clamped: cannot exceed scrollback.len()
state.scroll_up(100);
assert_eq!(state.scroll, state.scrollback.len());
fn scroll_down_decreases_offset_saturating_at_zero() {
state.scroll_down(1);
assert_eq!(state.scroll, 2);
state.scroll_down(100);
assert_eq!(state.scroll, 0);
fn reset_scroll_returns_to_zero() {
state.reset_scroll();
fn visible_scrollback_at_zero_shows_tail() {
for i in 0..10 {
let visible = state.visible_scrollback(3);
assert_eq!(visible, &["line 7", "line 8", "line 9"]);
fn visible_scrollback_scrolled_up_shows_older_lines() {
// end = 10 - 3 = 7, start = 7 - 3 = 4
assert_eq!(visible, &["line 4", "line 5", "line 6"]);
// Newest line is not visible
assert!(!visible.contains(&"line 9".to_string()));
fn visible_scrollback_over_scrolled_clamps_to_oldest_window() {
state.scroll_up(5);
assert_eq!(state.scroll, 5);
// Stored offset (5) exceeds len - height (2); effective offset clamps so
// the oldest 3 lines stay visible rather than emptying the window.
assert_eq!(visible, &state.scrollback[0..3]);
assert_eq!(visible, &["line 0", "line 1", "line 2"]);
assert!(!visible.is_empty());
fn visible_scrollback_height_larger_than_buffer_shows_all() {
state.push_scrollback("only");
let visible = state.visible_scrollback(100);
assert_eq!(visible, &["only"]);
fn visible_scrollback_empty_scrollback_is_empty() {
let state = console();
assert!(state.visible_scrollback(10).is_empty());
fn default_pane_is_prompt() {
assert_eq!(state.panes.focused(), PaneId::Prompt);
fn push_scrollback_caps_at_max_dropping_oldest() {
for i in 0..(MAX_SCROLLBACK_LINES + 5) {
assert_eq!(state.scrollback.len(), MAX_SCROLLBACK_LINES);
assert_eq!(state.scrollback.first().unwrap(), "line 5");
state.scrollback.last().unwrap(),
&format!("line {}", MAX_SCROLLBACK_LINES + 4)