Files
galaxy/app/src/terminal/model/block_test.rs
T

1426 lines
42 KiB
Rust

use std::{collections::HashMap, pin::pin, time::Duration};
use super::*;
use crate::{
ai::blocklist::agent_view::AgentViewState,
terminal::model::{
ansi::{Attr, Handler},
cell::Flags,
header_grid::PromptEndPoint,
session::SessionInfo,
test_utils::{create_test_block_with_grids, TestBlockBuilder},
},
test_util::mock_blockgrid,
};
use float_cmp::assert_approx_eq;
use futures_lite::stream::StreamExt;
impl float_cmp::ApproxEq for BlockSection {
type Margin = float_cmp::F64Margin;
fn approx_eq<M: Into<Self::Margin>>(self, other: Self, margin: M) -> bool {
// Use a minimum epsilon value of 3 * the smallest value that can be
// represented in an f64. This allows for a very small (but non-zero)
// amount of _accumulated_ floating-point error.
let mut margin: Self::Margin = margin.into();
if margin.epsilon < f64::EPSILON * 3. {
margin = margin.epsilon(f64::EPSILON * 3.);
}
match (self, other) {
(BlockSection::PromptAndCommandGrid(r1), BlockSection::PromptAndCommandGrid(r2)) => {
r1.approx_eq(r2, margin)
}
(BlockSection::OutputGrid(r1), BlockSection::OutputGrid(r2)) => {
r1.approx_eq(r2, margin)
}
(a, b) => std::mem::discriminant(&a) == std::mem::discriminant(&b),
}
}
}
#[test]
pub fn test_find() {
let mut block = TestBlockBuilder::new().build();
block.precmd(PrecmdValue::default());
block.start();
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 3.);
assert_approx_eq!(
BlockSection,
block.find(Lines::zero()),
BlockSection::PaddingTop
);
assert_approx_eq!(
BlockSection,
block.find(0.5.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.0.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.5.into_lines()),
BlockSection::PromptAndCommandGrid(0.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(2.0.into_lines()),
BlockSection::PaddingBottom
);
assert_approx_eq!(
BlockSection,
block.find(3.5.into_lines()),
BlockSection::NotContained
);
// Add a few lines to the command grid.
block.header_grid.command_grid_linefeed();
block.header_grid.command_grid_linefeed();
block.header_grid.command_grid_linefeed();
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 6.);
assert_approx_eq!(
BlockSection,
block.find(Lines::zero()),
BlockSection::PaddingTop
);
assert_approx_eq!(
BlockSection,
block.find(0.5.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.0.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.5.into_lines()),
BlockSection::PromptAndCommandGrid(0.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(2.01.into_lines()),
BlockSection::PromptAndCommandGrid(1.01.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(2.5.into_lines()),
BlockSection::PromptAndCommandGrid(1.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(3.5.into_lines()),
BlockSection::PromptAndCommandGrid(2.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(4.5.into_lines()),
BlockSection::PromptAndCommandGrid(3.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(5.0.into_lines()),
BlockSection::PaddingBottom
);
assert_approx_eq!(
BlockSection,
block.find(8.0.into_lines()),
BlockSection::NotContained
);
// Add lines to the output grid. The command grid will now shrink to be 3 lines and the
// output grid will be 2 lines.
block.header_grid.finish_command_grid();
block.output_grid.start();
block.output_grid.linefeed();
block.output_grid.linefeed();
block
.output_grid
.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert_eq!(block.header_grid.prompt_and_command_number_of_rows(), 3);
assert_eq!(block.output_grid.len(), 3);
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 8.5);
assert_approx_eq!(
BlockSection,
block.find(Lines::zero()),
BlockSection::PaddingTop
);
assert_approx_eq!(
BlockSection,
block.find(0.5.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(0.8.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.0.into_lines()),
BlockSection::PromptAndCommandGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(1.5.into_lines()),
BlockSection::PromptAndCommandGrid(0.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(2.01.into_lines()),
BlockSection::PromptAndCommandGrid(1.01.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(2.5.into_lines()),
BlockSection::PromptAndCommandGrid(1.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(3.5.into_lines()),
BlockSection::PromptAndCommandGrid(2.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(4.0.into_lines()),
BlockSection::PaddingMiddle
);
assert_approx_eq!(
BlockSection,
block.find(4.5.into_lines()),
BlockSection::OutputGrid(Lines::zero())
);
assert_approx_eq!(
BlockSection,
block.find(5.5.into_lines()),
BlockSection::OutputGrid(1.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(6.0.into_lines()),
BlockSection::OutputGrid(1.5.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(6.5.into_lines()),
BlockSection::OutputGrid(2.into_lines())
);
assert_approx_eq!(
BlockSection,
block.find(7.5.into_lines()),
BlockSection::PaddingBottom
);
assert_approx_eq!(
BlockSection,
block.find(8.5.into_lines()),
BlockSection::NotContained
);
assert_approx_eq!(
BlockSection,
block.find(10.5.into_lines()),
BlockSection::NotContained
);
let mut block = TestBlockBuilder::new().build();
block.precmd(PrecmdValue::default());
block.start();
block.header_grid.command_grid_linefeed();
block.header_grid.command_grid_linefeed();
block.header_grid.finish_command_grid();
block.output_grid.start();
block.output_grid.linefeed();
block.output_grid.linefeed();
block
.output_grid
.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert_eq!(block.header_grid.prompt_and_command_number_of_rows(), 2);
assert_eq!(block.output_grid.len(), 3);
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 7.5);
assert_approx_eq!(
BlockSection,
block.find(3.5.into_lines()),
BlockSection::OutputGrid(Lines::zero())
);
}
#[test]
pub fn test_long_running_block_bottom_padding() {
galaxyui::r#async::block_on(async {
let mut block = TestBlockBuilder::new().build();
block.precmd(Default::default());
block.start();
for c in "command".chars() {
block.input(c);
}
block.preexec(Default::default());
for c in "output".chars() {
block.input(c);
}
// Initially, the padding is still there because we aren't yet in long-running mode
assert!(!block.is_active_and_long_running());
assert!(block.padding_bottom() > LONG_RUNNING_BOTTOM_PADDING_LINES.into_lines());
// After the long running duration, the block should switch
let duration = LONG_RUNNING_COMMAND_DURATION_MS + 1;
galaxyui::r#async::Timer::after(Duration::from_millis(duration)).await;
assert!(block.is_active_and_long_running());
assert!(block.padding_bottom() == LONG_RUNNING_BOTTOM_PADDING_LINES.into_lines());
});
}
// Tests that the command grid has a non-zero height even if `preexec` is never called.
#[test]
pub fn test_precmd_no_preexec() {
let mut block = TestBlockBuilder::new().build();
block.start();
block.precmd(PrecmdValue::default());
for c in "command".chars() {
block.input(c);
}
block.carriage_return();
block.linefeed();
for c in "error".chars() {
block.input(c);
}
block.finish(0);
assert_eq!("command\nerror", block.contents_to_string());
}
// Tests that the command grid text is all bolded.
#[test]
pub fn test_command_grid_bold() {
let mut block = TestBlockBuilder::new().build();
block.start();
block.precmd(PrecmdValue::default());
// We should have the BOLD flag enabled for commands, once we've started the command grid.
assert!(block
.header_grid
.command_cursor_flags()
.contains(Flags::BOLD));
for c in "command".chars() {
block.input(c);
}
block.finish(0);
assert_eq!("command", block.contents_to_string());
}
// Tests that the command grid text is still bolded after RESET attribute.
#[test]
pub fn test_command_grid_bold_after_reset() {
let mut block = TestBlockBuilder::new().build();
block.start();
block.precmd(PrecmdValue::default());
// We should have the BOLD flag enabled for commands, once we've started the command grid.
assert!(block
.header_grid
.command_cursor_flags()
.contains(Flags::BOLD));
for c in "command".chars() {
block.input(c);
}
// Even after a Reset, we should still re-enable the BOLD flag.
block.terminal_attribute(Attr::Reset);
assert!(block
.header_grid
.command_cursor_flags()
.contains(Flags::BOLD));
block.finish(0);
assert_eq!("command", block.contents_to_string());
}
// Tests that the command grid has non-zero height even if the shell does not echo a linefeed
// after the user inputs an empty command (this happens in recent Bash versions)
#[test]
pub fn test_empty_command() {
let mut block = TestBlockBuilder::new().build();
block.start();
block.precmd(PrecmdValue::default());
block.finish(0);
assert!(!block.is_command_empty());
}
#[test]
pub fn test_failed_block() {
let mut block = TestBlockBuilder::new().build();
block.precmd(PrecmdValue::default());
block.preexec(Default::default());
block.finish(1 /* exit_code */);
assert!(block.has_failed());
let mut block = TestBlockBuilder::new().build();
block.precmd(PrecmdValue::default());
block.finish(1 /* exit_code */);
// The block should not be marked as failed since execution never started.
assert!(!block.has_failed());
}
#[test]
fn test_non_error_exit_codes() {
let mut block = TestBlockBuilder::new().build();
block.precmd(Default::default());
block.preexec(Default::default());
block.finish(130 /* exit_code */);
// The block should not be marked as failed since it was interrupted by user action (SIGINT)
assert!(!block.has_failed());
let mut block = TestBlockBuilder::new().build();
block.precmd(Default::default());
block.preexec(Default::default());
block.finish(141 /* exit_code */);
// The block should not be marked as failed since it was interrupted by a pipe closing (SIGPIPE)
assert!(!block.has_failed());
}
#[test]
pub fn test_block_height_non_bootstrapped_block() {
let mut block = TestBlockBuilder::new().build();
// Add a few lines to the grid.
block.linefeed();
block.linefeed();
block.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// The block is empty since it was never started.
assert!(block.is_empty(&AgentViewState::Inactive));
block.start();
// The block should be non-empty even though it wasn't boostrapped.
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 5.);
}
#[test]
fn test_background_block() {
let mut block = TestBlockBuilder::new().build();
block.set_honor_ps1(true);
block.start_background(None);
assert_eq!(block.state(), BlockState::Background);
for c in "background output".chars() {
block.input(c);
}
block.carriage_return();
block.linefeed();
block.finish(0);
assert_eq!(
block.output_grid.contents_to_string(true, None),
"background output\r\n"
);
// Background blocks have the usual top and bottom padding, but no
// between-grid padding because there's only one grid.
assert_lines_approx_eq!(block.output_grid_displayed_height(), 3);
assert_lines_approx_eq!(block.height(&AgentViewState::Inactive), 4.2);
}
#[test]
pub fn test_block_duration_formatting() {
// .01 seconds
let d1 = chrono::Duration::milliseconds(10);
assert_eq!(Block::format_duration(d1), " (0.01s)");
// .123 seconds
let d2 = chrono::Duration::milliseconds(123);
assert_eq!(Block::format_duration(d2), " (0.123s)");
// 3.143 seconds
let d3 = chrono::Duration::milliseconds(3143);
assert_eq!(Block::format_duration(d3), " (3.143s)");
// 10.44 seconds
let d4 = chrono::Duration::milliseconds(10440);
assert_eq!(Block::format_duration(d4), " (10.44s)");
// 25m 23.423 seconds
let d5 = chrono::Duration::milliseconds(1523423);
assert_eq!(Block::format_duration(d5), " (25m 23.42s)");
// 3h 25m 8.133 seconds
let d6 = chrono::Duration::milliseconds(12308133);
assert_eq!(Block::format_duration(d6), " (3h 25m 8s)");
// 22h 51m 48.902 seconds
let d7 = chrono::Duration::milliseconds(82308902);
assert_eq!(Block::format_duration(d7), " (22h 51m 49s)");
let d8 = chrono::Duration::milliseconds(3604114);
assert_eq!(Block::format_duration(d8), " (1h 4s)");
}
#[test]
pub fn test_block_emits_block_completed_event_for_in_band_command() {
let (events_tx, events_rx) = async_channel::unbounded();
let event_proxy = ChannelEventListener::builder_for_test()
.with_terminal_events_tx(events_tx)
.build();
let mut block = TestBlockBuilder::new()
.with_event_proxy(event_proxy)
.build();
block.start_for_in_band_command();
block.precmd(Default::default());
block.preexec(PreexecValue {
command: "warp_run_generator_command 1234 foo".to_owned(),
});
block.finish(0);
events_rx.close();
let block_completed_event =
galaxyui::r#async::block_on(pin!(events_rx).find(|event| match event {
Event::BlockCompleted(event) => matches!(event.block_type, BlockType::InBandCommand),
_ => false,
}));
assert!(block_completed_event.is_some());
}
/// Tests the multiline lprompt and multiline command case for selecting text across grids.
#[test]
fn test_selection_bounds_all_grids_multiline_lprompt_command() {
let block_index = BlockIndex::zero();
// Grid contents:
// -----
// lprompt1
// lprompt2%cmd1 rprompt
// cmd2
// cmd3
// output1
// output2
// -----
let mut prompt_and_command_grid = mock_blockgrid("lprompt1\r\nlprompt2%cmd1\r\ncmd2\r\ncmd3");
prompt_and_command_grid.finish();
let mut rprompt_grid = mock_blockgrid("rprompt");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
block.set_raw_prompt_end_point(Some(PromptEndPoint::PromptEnd {
point: Point::new(1, 8),
has_extra_trailing_newline: false,
}));
// Test cross-over across all sections (lprompt to output).
let all_grids = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(2, 0)),
);
assert_eq!(
all_grids,
"lprompt1\nlprompt2%cmd1\nrprompt\ncmd2\ncmd3\noutput1\noutput2"
);
// Test individual sections.
let lprompt_only = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 2)),
BlockGridPoint::PromptAndCommand(Point::new(1, 3)),
);
assert_eq!(lprompt_only, "rompt1\nlpro");
let rprompt_only = block.bounds_to_string(
BlockGridPoint::Rprompt(Point::new(0, 1)),
BlockGridPoint::Rprompt(Point::new(0, 4)),
);
assert_eq!(rprompt_only, "prom");
let bottom_command_only = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(2, 1)),
BlockGridPoint::PromptAndCommand(Point::new(3, 2)),
);
assert_eq!(bottom_command_only, "md2\ncmd");
let output_only = block.bounds_to_string(
BlockGridPoint::Output(Point::new(0, 3)),
BlockGridPoint::Output(Point::new(0, 6)),
);
assert_eq!(output_only, "put1");
// Test cross-overs across sections.
let lprompt_to_rprompt = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 2)),
BlockGridPoint::PromptAndCommand(Point::new(1, 2)),
);
assert_eq!(lprompt_to_rprompt, "rompt1\nlpr");
let lprompt_to_bottom_command = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 2)),
BlockGridPoint::PromptAndCommand(Point::new(3, 2)),
);
assert_eq!(
lprompt_to_bottom_command,
"rompt1\nlprompt2%cmd1\nrprompt\ncmd2\ncmd"
);
let rprompt_to_bottom_command = block.bounds_to_string(
BlockGridPoint::Rprompt(Point::new(0, 0)),
BlockGridPoint::PromptAndCommand(Point::new(3, 2)),
);
assert_eq!(rprompt_to_bottom_command, "rprompt\ncmd2\ncmd");
let rprompt_to_output = block.bounds_to_string(
BlockGridPoint::Rprompt(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(0, 3)),
);
assert_eq!(rprompt_to_output, "rprompt\ncmd2\ncmd3\noutp");
let bottom_command_to_output = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(2, 1)),
BlockGridPoint::Output(Point::new(1, 1)),
);
assert_eq!(bottom_command_to_output, "md2\ncmd3\noutput1\nou");
// Test a few "impossible cases" and ensure they return an empty string.
let impossible_output_to_rprompt = block.bounds_to_string(
BlockGridPoint::Output(Point::new(1, 0)),
BlockGridPoint::Rprompt(Point::new(1, 1)),
);
assert_eq!(impossible_output_to_rprompt, "");
let impossible_output_to_lprompt = block.bounds_to_string(
BlockGridPoint::Output(Point::new(1, 0)),
BlockGridPoint::PromptAndCommand(Point::new(0, 1)),
);
assert_eq!(impossible_output_to_lprompt, "");
}
/// Tests the single line lprompt and command case for text selection across grids.
#[test]
fn test_selection_bounds_all_grids_single_line_lprompt_command() {
let block_index = BlockIndex::zero();
// Grid contents:
// -----
// lprompt1%cmd1 rprompt
// output1
// output2
// -----
let mut prompt_and_command_grid = mock_blockgrid("lprompt%cmd1");
prompt_and_command_grid.finish();
let mut rprompt_grid = mock_blockgrid("rprompt");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
block.set_raw_prompt_end_point(Some(PromptEndPoint::PromptEnd {
point: Point::new(0, 7),
has_extra_trailing_newline: false,
}));
// Test cross-over across all sections (lprompt to output).
let all_grids = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(2, 0)),
);
assert_eq!(all_grids, "lprompt%cmd1\nrprompt\noutput1\noutput2");
}
/// Tests the single line lprompt, with trailing newline, and command case for text selection across grids.
#[test]
fn test_selection_bounds_all_grids_single_line_lprompt_trailing_newline_command() {
let block_index = BlockIndex::zero();
// Grid contents (lprompt has trailing newline):
// -----
// lprompt1%
// cmd1 rprompt
// output1
// output2
// -----
let mut prompt_and_command_grid = mock_blockgrid("lprompt%\r\ncmd1");
prompt_and_command_grid.finish();
let mut rprompt_grid = mock_blockgrid("rprompt");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid.clone(),
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
// We are including the \n in the lprompt, not the command!
block.set_raw_prompt_end_point(Some(PromptEndPoint::PromptEnd {
point: Point::new(0, 7),
has_extra_trailing_newline: true,
}));
// Test cross-over across all sections (lprompt to output).
let all_grids = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(2, 0)),
);
assert_eq!(all_grids, "lprompt%\ncmd1\nrprompt\noutput1\noutput2");
}
// Tests the single line lprompt and command, with starting newline, case for text selection across grids.
#[test]
fn test_selection_bounds_all_grids_single_line_lprompt_command_starting_newline() {
let block_index = BlockIndex::zero();
// Grid contents (command has starting newline):
// -----
// lprompt1% rprompt
// cmd1
// output1
// output2
// -----
let mut prompt_and_command_grid = mock_blockgrid("lprompt%\r\ncmd1");
prompt_and_command_grid.finish();
let mut rprompt_grid = mock_blockgrid("rprompt");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid.clone(),
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
// We are including the \n in the command, not the lprompt!
block.set_raw_prompt_end_point(Some(PromptEndPoint::PromptEnd {
point: Point::new(0, 7),
has_extra_trailing_newline: false,
}));
// Test cross-over across all sections (lprompt to output).
let all_grids = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(2, 0)),
);
assert_eq!(all_grids, "lprompt%\nrprompt\ncmd1\noutput1\noutput2");
}
/// Tests the multiline lprompt case, with no command content (where prompt end point is NOT defined).
#[test]
fn test_selection_bounds_all_grids_multiline_lprompt_no_command() {
let block_index = BlockIndex::zero();
// Grid contents:
// -----
// lprompt1
// lprompt2% rprompt
// output1
// output2
// -----
// Note: practically, this case is generally not possible, since a command needs to be run for output, but this is used for testing purposes and it
// could happen in a case similar to background output block (though, practically, those blocks don't have prompts).
let mut prompt_and_command_grid = mock_blockgrid("lprompt1\r\nlprompt2%");
prompt_and_command_grid.finish();
let mut rprompt_grid = mock_blockgrid("rprompt");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("output1\r\noutput2\r\n");
output_grid.finish();
let block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
// Test cross-over across all sections (lprompt to output).
let all_grids = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(2, 0)),
);
assert_eq!(all_grids, "lprompt1\nlprompt2%\nrprompt\noutput1\noutput2");
// Test individual sections.
let lprompt_only = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 2)),
BlockGridPoint::PromptAndCommand(Point::new(1, 3)),
);
assert_eq!(lprompt_only, "rompt1\nlpro");
let rprompt_only = block.bounds_to_string(
BlockGridPoint::Rprompt(Point::new(0, 1)),
BlockGridPoint::Rprompt(Point::new(0, 4)),
);
assert_eq!(rprompt_only, "prom");
let output_only = block.bounds_to_string(
BlockGridPoint::Output(Point::new(0, 3)),
BlockGridPoint::Output(Point::new(0, 6)),
);
assert_eq!(output_only, "put1");
// Test cross-overs across sections.
let lprompt_to_rprompt = block.bounds_to_string(
BlockGridPoint::PromptAndCommand(Point::new(0, 2)),
BlockGridPoint::PromptAndCommand(Point::new(1, 2)),
);
assert_eq!(lprompt_to_rprompt, "rompt1\nlpr");
let rprompt_to_output = block.bounds_to_string(
BlockGridPoint::Rprompt(Point::new(0, 0)),
BlockGridPoint::Output(Point::new(0, 3)),
);
assert_eq!(rprompt_to_output, "rprompt\noutp");
// Test a few "impossible cases" and ensure they return an empty string.
let impossible_output_to_rprompt = block.bounds_to_string(
BlockGridPoint::Output(Point::new(1, 0)),
BlockGridPoint::Rprompt(Point::new(1, 1)),
);
assert_eq!(impossible_output_to_rprompt, "");
let impossible_output_to_lprompt = block.bounds_to_string(
BlockGridPoint::Output(Point::new(1, 0)),
BlockGridPoint::PromptAndCommand(Point::new(0, 1)),
);
assert_eq!(impossible_output_to_lprompt, "");
}
/// Tests cloning "typeahead" content from background output grid into the combined grid.
/// Specifically, this tests the user behavior of typeahead when pressing ENTER after typing a command.
#[test]
fn test_clone_command_from_blockgrid() {
let block_index = BlockIndex::zero();
// Grid contents (copying INTO this Grid):
// -----
// lprompt1
// lprompt2%
// -----
// Grid to copy contents FROM:
// -----
// clone1
// clone2
// clone3
// -----
// Final expected grid (internals):
// -----
// lprompt1
// lprompt2% (soft-wrap)
// clone1 (hard-wrap)
// clone2 (hard-wrap)
// clone3 (hard-wrap)
// -----
let prompt_and_command_grid = mock_blockgrid("lprompt1\r\nlprompt2%");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
let mut grid_to_clone = mock_blockgrid("clone1\r\nclone2\r\nclone3");
grid_to_clone.finish();
block
.header_grid
.prompt_and_command_grid_mut()
.set_mode(ansi::Mode::LineFeedNewLine);
block.copy_command_grid(&grid_to_clone);
let new_content = block.prompt_and_command_grid().to_string(
false,
None,
RespectObfuscatedSecrets::Yes,
false,
RespectDisplayedOutput::Yes,
);
assert_eq!(new_content, "lprompt1\nlprompt2%clone1\nclone2\nclone3");
}
#[test]
fn test_clone_command_from_blockgrid_long() {
let block_index = BlockIndex::zero();
// Grid contents (copying INTO this Grid):
// -----
// lprompt1
// lprompt2%
// -----
// Grid to copy contents FROM:
// -----
// clone1_long
// clone2_long
// clone3_long
// -----
// Final expected grid (internals):
// -----
// lprompt1
// lprompt2% (soft-wrap)
// clone1_lo (soft-wrap)
// ng (hard-wrap)
// clone2_lo (soft-wrap)
// ng (hard-wrap)
// clone3_lo (soft-wrap)
// ng
// -----
let prompt_and_command_grid = mock_blockgrid("lprompt1\r\nlprompt2%");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
let mut grid_to_clone = mock_blockgrid("clone1_long\r\nclone2_long\r\nclone3_long");
grid_to_clone.finish();
block
.header_grid
.prompt_and_command_grid_mut()
.set_mode(ansi::Mode::LineFeedNewLine);
block.copy_command_grid(&grid_to_clone);
let new_content = block.prompt_and_command_grid().to_string(
false,
None,
RespectObfuscatedSecrets::Yes,
false,
RespectDisplayedOutput::Yes,
);
assert_eq!(
new_content,
"lprompt1\nlprompt2%clone1_long\nclone2_long\nclone3_long"
);
}
#[test]
fn test_clone_command_from_blockgrid_from_empty() {
let block_index = BlockIndex::zero();
// Grid contents (copying INTO this Grid):
// -----
// lprompt1
// lprompt2%
// -----
// Grid to copy contents FROM:
// -----
// -----
// Final expected grid (internals):
// -----
// lprompt1 (hard-wrap)
// lprompt2%
// -----
let prompt_and_command_grid = mock_blockgrid("lprompt1\r\nlprompt2%");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
let mut grid_to_clone = mock_blockgrid("");
grid_to_clone.finish();
block
.header_grid
.prompt_and_command_grid_mut()
.set_mode(ansi::Mode::LineFeedNewLine);
block.copy_command_grid(&grid_to_clone);
let new_content = block.prompt_and_command_grid().to_string(
false,
None,
RespectObfuscatedSecrets::Yes,
false,
RespectDisplayedOutput::Yes,
);
assert_eq!(new_content, "lprompt1\nlprompt2%");
}
#[test]
fn test_clone_command_from_blockgrid_to_empty() {
let block_index = BlockIndex::zero();
// Grid contents (copying INTO this Grid):
// -----
// -----
// Grid to copy contents FROM:
// -----
// clone1
// clone2
// clone3
// -----
// Final expected grid (internals):
// -----
// clone1 (hard-wrap)
// clone2 (hard-wrap)
// clone3
// -----
let prompt_and_command_grid = mock_blockgrid("");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
let mut grid_to_clone = mock_blockgrid("clone1\r\nclone2\r\nclone3");
grid_to_clone.finish();
block
.header_grid
.prompt_and_command_grid_mut()
.set_mode(ansi::Mode::LineFeedNewLine);
block.copy_command_grid(&grid_to_clone);
let new_content = block.prompt_and_command_grid().to_string(
false,
None,
RespectObfuscatedSecrets::Yes,
false,
RespectDisplayedOutput::Yes,
);
assert_eq!(new_content, "clone1\nclone2\nclone3");
}
/// Tests is_command_empty for an empty command with the combined grid.
#[test]
fn test_command_is_empty_combined_grid() {
let block_index = BlockIndex::zero();
// Grid contents:
// -----
// -----
let prompt_and_command_grid = mock_blockgrid("");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
block.start();
block.finish_command_grid();
assert!(
block.is_command_empty(),
"Command should be considered empty!"
);
}
/// Tests is_command_empty for an non-empty command with the combined grid.
#[test]
fn test_command_is_not_empty_combined_grid() {
let block_index = BlockIndex::zero();
// Grid contents:
// -----
// command1 (linefeed at the end from finishing the block)
// -----
let prompt_and_command_grid = mock_blockgrid("command1");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
block.start();
block.finish(0);
assert!(
!block.is_command_empty(),
"Command should not be considered empty!"
);
}
/// Regression test (CORE-1947): checks Warp prompt case for is_command_empty. Specifically,
/// even if the combined grid's content _exactly_ matches the prompt grid's content (which is used
/// for PS1 preview), we should NOT consider the command to be empty. The underlying cursor check should
/// be against (0, 0) in the combined grid, for the Warp prompt case, rather than checking against the
/// prompt grid (which we do in the PS1 active case).
#[test]
fn test_command_is_empty_warp_prompt() {
let block_index = BlockIndex::zero();
// Combined grid contents:
// -----
// warp_prompt
// abcde
// -----
// Prompt grid contents:
// -----
// abcde
// -----
let prompt_and_command_grid = mock_blockgrid("abcde");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
// Note that the prompt_grid's finished cursor will be at (0, 4) with the content below.
// This will be the exact same as the combined grid's cursor at (0, 4), which will
// check the regression case (we should NOT consider the command to be empty!).
let mut prompt_grid = mock_blockgrid("abcde");
prompt_grid.finish();
// Note that we are indicating Warp prompt, not PS1 here!
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
false, /* honor_ps1 */
);
block.set_prompt_grid(prompt_grid);
block.start();
block.finish_command_grid();
assert!(
!block.is_command_empty(),
"Command should not be considered empty!"
);
}
/// Tests is_command_empty behavior with PS1 prompt _and_ command with combined grid.
#[test]
fn test_command_is_empty_combined_grid_prompt_and_command() {
let block_index = BlockIndex::zero();
// Combined grid contents:
// -----
// prompt1command1
// -----
// Prompt grid contents:
// -----
// prompt1
// -----
let prompt_and_command_grid = mock_blockgrid("prompt1command1");
let mut rprompt_grid = mock_blockgrid("");
rprompt_grid.finish();
let mut output_grid = mock_blockgrid("");
output_grid.finish();
let mut prompt_grid = mock_blockgrid("prompt1");
prompt_grid.finish();
let mut block = create_test_block_with_grids(
block_index,
prompt_and_command_grid,
rprompt_grid,
output_grid,
true, /* honor_ps1 */
);
block.set_prompt_grid(prompt_grid);
block.start();
block.finish(0);
assert!(
!block.is_command_empty(),
"Command should not be considered empty!"
);
}
#[test]
fn test_top_level_command() {
let mut sessions = Sessions::new_for_test();
sessions.register_session_for_test(SessionInfo::new_for_test().with_id(0));
let mut block = TestBlockBuilder::new()
.with_size_info(SizeInfo::new_without_font_metrics(1, 20))
.build();
block.start();
block.precmd(PrecmdValue {
session_id: Some(0),
..Default::default()
});
for c in "git status".chars() {
block.input(c);
}
assert_eq!(block.top_level_command(&sessions), Some("git".to_string()));
let mut block = TestBlockBuilder::new()
.with_size_info(SizeInfo::new_without_font_metrics(1, 20))
.build();
block.start();
block.precmd(PrecmdValue {
session_id: Some(0),
..Default::default()
});
for c in "PAGER=0 git status".chars() {
block.input(c);
}
assert_eq!(block.top_level_command(&sessions), Some("git".to_string()));
}
#[test]
fn test_top_level_command_with_aliases() {
let mut sessions = Sessions::new_for_test();
sessions.register_session_for_test(SessionInfo::new_for_test().with_id(0).with_aliases(
HashMap::from_iter([
("git".into(), String::from("git2")),
("gs".into(), String::from("git status")),
("gl".into(), String::from("PAGER=0 git log")),
]),
));
let mut block = TestBlockBuilder::new()
.with_size_info(SizeInfo::new_without_font_metrics(1, 20))
.build();
block.start();
block.precmd(PrecmdValue {
session_id: Some(0),
..Default::default()
});
for c in "git status".chars() {
block.input(c);
}
assert_eq!(block.top_level_command(&sessions), Some("git2".to_string()));
let mut block = TestBlockBuilder::new()
.with_size_info(SizeInfo::new_without_font_metrics(1, 20))
.build();
block.start();
block.precmd(PrecmdValue {
session_id: Some(0),
..Default::default()
});
for c in "gs --ignored".chars() {
block.input(c);
}
assert_eq!(block.top_level_command(&sessions), Some("git".to_string()));
let mut block = TestBlockBuilder::new()
.with_size_info(SizeInfo::new_without_font_metrics(1, 20))
.build();
block.start();
block.precmd(PrecmdValue {
session_id: Some(0),
..Default::default()
});
for c in "PAGER= gl --flag".chars() {
block.input(c);
}
assert_eq!(block.top_level_command(&sessions), Some("git".to_string()));
}
#[test]
fn test_mark_end_of_prompt_with_some_rows_in_flat_storage() {
use galaxy_terminal::model::grid::Dimensions as _;
let mut block = TestBlockBuilder::new()
// Set the number of visible rows to 1, so that the first row of the
// prompt ends up in flat storage.
.with_size_info(SizeInfo::new_without_font_metrics(1, 40))
.with_honor_ps1(true)
.build();
block.precmd(PrecmdValue {
ps1: Some(hex::encode("prompt1\r\n")),
honor_ps1: Some(true),
..Default::default()
});
block.start();
block.finish(0);
// Resize the grid so that everything ends up in grid storage.
let rows = block
.header_grid
.prompt_and_command_grid()
.grid_handler()
.total_rows();
block.resize(SizeInfo::new_without_font_metrics(rows, 40));
// Assert that the end-of-prompt marker is set where we expect it to be.
// This ensures that we properly set it within flat storage when initially
// setting the end-of-prompt marker during precmd().
let prompt_end_point = block.header_grid.prompt_end_point();
let Some(PromptEndPoint::PromptEnd { point, .. }) = prompt_end_point else {
panic!("Expected to have a well-defined prompt end point; got: {prompt_end_point:?}");
};
assert_eq!(point, Point::new(0, 39));
assert!(block.header_grid.prompt_and_command_grid().grid_storage()[point].is_end_of_prompt());
}
#[test]
fn test_restored_block_was_local() {
// Test default value is None
let mut block = TestBlockBuilder::new()
.with_bootstrap_stage(BootstrapStage::RestoreBlocks)
.build();
assert_eq!(block.restored_block_was_local(), None);
// Test setting to true
block.set_restored_block_was_local(true);
assert_eq!(block.restored_block_was_local(), Some(true));
// Test changing value
block.set_restored_block_was_local(false);
assert_eq!(block.restored_block_was_local(), Some(false));
// Test with another new block (default is still None)
let block = TestBlockBuilder::new()
.with_bootstrap_stage(BootstrapStage::RestoreBlocks)
.build();
assert_eq!(block.restored_block_was_local(), None);
}
#[test]
fn test_deserialize_legacy_agent_view_visibility_agent_variant() {
let origin_conversation_id = AIConversationId::new();
let json = format!("{{\"Agent\":{{\"conversation_id\":\"{origin_conversation_id}\"}}}}");
let visibility: SerializedAgentViewVisibility = serde_json::from_str(&json).unwrap();
match visibility {
SerializedAgentViewVisibility::Agent {
origin_conversation_id: parsed_origin_conversation_id,
pending_other_conversation_ids,
other_conversation_ids,
} => {
assert_eq!(parsed_origin_conversation_id, origin_conversation_id);
assert!(pending_other_conversation_ids.is_empty());
assert!(other_conversation_ids.is_empty());
}
_ => panic!("Expected agent visibility"),
}
}
#[test]
fn test_calculate_optimal_row_counts_wide_terminal() {
// Terminal width >= 150 should return default values
let (top, bottom) = calculate_optimal_row_counts(150, 100, 200);
assert_eq!(top, 100);
assert_eq!(bottom, 200);
let (top, bottom) = calculate_optimal_row_counts(200, 50, 100);
assert_eq!(top, 50);
assert_eq!(bottom, 100);
}
#[test]
fn test_calculate_optimal_row_counts_narrow_terminal() {
// Terminal width < 150 should optimize based on target total cells
let (top, bottom) = calculate_optimal_row_counts(75, 100, 200);
// Target total cells = (100 + 200) * 150 = 45,000
// Total rows for width 75 = 45,000 / 75 = 600
// Top rows = (600 * 100) / 300 = 200
// Bottom rows = 600 - 200 = 400
assert_eq!(top, 200);
assert_eq!(bottom, 400);
}
#[test]
fn test_calculate_optimal_row_counts_very_narrow_terminal() {
// Test edge case with very narrow terminal
let (top, bottom) = calculate_optimal_row_counts(30, 100, 200);
// Target total cells = (100 + 200) * 150 = 45,000
// Total rows for width 30 = 45,000 / 30 = 1,500
// Top rows = (1,500 * 100) / 300 = 500
// Bottom rows = 1,500 - 500 = 1,000
assert_eq!(top, 500);
assert_eq!(bottom, 1000);
}