use super::*;
#[test]
fn wraps_unicode_sequence_by_display_width() {
let text = "- 界 Existing worktree changes remain";
let max_width = UnicodeWidthStr::width(text) - 1;
let rows = wrap_spans_to_width(vec![Span::raw(text.to_string())], max_width);
assert_eq!(rows.len(), 2);
assert_eq!(
rows.iter().map(|row| spans_text(row)).collect::<String>(),
text
);
assert!(
rows.iter().all(|row| spans_display_width(row) <= max_width),
"rows exceeded {max_width}: {rows:?}"
);
}
#[test]
fn narrow_wrap_preserves_wide_graphemes_in_copy_projection() {
for width in [1, 2] {
for text in ["界", "界界", "e\u{0301}"] {
let rows = wrap_spans_to_width(vec![Span::raw(text.to_string())], width);
assert_eq!(
rows.iter().map(|row| spans_text(row)).collect::<String>(),
text,
"width {width}, text {text:?}"
);
assert!(!rows.is_empty());
}
}
}
#[test]
fn narrow_assistant_copy_projection_preserves_wide_graphemes() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "界 界界 e\u{0301}".to_string(),
});
for width in [1, 2] {
let copied = visual_text_for_width(&state, width);
assert!(
copied.contains("界 界界 e\u{0301}"),
"width {width}: {copied:?}"
);
assert!(
visual_lines(&state, width)
.iter()
.filter(|line| line.copyable)
.all(|line| {
!line.copy_text.is_empty()
|| line
.copy_byte_range
.is_some_and(|(start, end)| start == end)
})
);
}
}
#[test]
fn wide_card_borders_hold_with_grapheme_clusters() {
let state = linked_tool_state(
"call_read",
"read",
"read unicode",
ActivityStatus::Success,
Vec::new(),
"界⌨ e\u{0301}".repeat(8).as_str(),
);
for line in visual_lines(&state, CARD_LAYOUT_MIN_WIDTH) {
let text = line_text(&line.line);
assert_eq!(
UnicodeWidthStr::width(text.as_str()),
usize::from(CARD_LAYOUT_MIN_WIDTH),
"{text}"
);
if text.starts_with('│') {
assert!(text.ends_with('│'), "{text}");
}
}
}
#[test]
fn bounded_lines_truncates_long_display_width_without_breaking_graphemes() {
let input = format!("{}\nkept", "界".repeat(140));
let bounded = bounded_lines(&input, 2);
let first = bounded.lines().next().unwrap();
assert!(first.ends_with("… [truncated]"), "{first}");
assert!(UnicodeWidthStr::width(first) <= PREVIEW_LINE_MAX_DISPLAY_WIDTH);
assert!(first.is_char_boundary(first.trim_end_matches("… [truncated]").len()));
assert!(bounded.contains("\nkept"), "{bounded}");
}
#[test]
fn card_bands_fill_width_and_wrap_body_rows() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu".into(),
});
let lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let rendered = lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>();
assert!(rendered.iter().any(|line| line.contains("User: ")));
assert!(rendered.len() > 1, "body should wrap: {rendered:?}");
for line in &rendered {
assert_eq!(
UnicodeWidthStr::width(line.as_str()),
usize::from(CARD_LAYOUT_MIN_WIDTH),
"{line}"
);
assert!(!line.contains(['┌', '┐', '└', '┘']));
}
assert_eq!(
lines[0].line.spans[0].style.bg,
Some(MissionControlTheme::default().surface_panel())
);
assert!(
lines[0]
.line
.spans
.iter()
.all(|span| span.style.bg == Some(MissionControlTheme::default().surface_panel()))
);
}
#[test]
fn assistant_markdown_code_gutter_stays_visual_but_not_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```rust\nfn main() {}\n```".into(),
});
let rendered = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH)
.into_iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("│ fn main() {}"), "{rendered}");
let copy_text = state.transcript_visual_text();
assert!(copy_text.contains("fn main() {}"), "{copy_text}");
assert!(!copy_text.contains("│ fn main() {}"), "{copy_text}");
}
#[test]
fn compact_assistant_markdown_code_gutter_stays_visual_but_not_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let compact_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH - 1);
let rendered = compact_lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ alpha"), "{rendered}");
let copy_text = compact_lines
.iter()
.filter(|line| line.copyable)
.map(|line| line.copy_text.as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(copy_text.contains("alpha"), "{copy_text}");
assert!(!copy_text.contains("│ alpha"), "{copy_text}");
}
#[test]
fn compact_code_gutter_visual_selection_copy_starts_at_visible_code_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH - 1;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ alpha"), "{rendered}");
let alpha_row = visual_lines(&state, width)
.iter()
.position(|line| line_text(&line.line).contains(" │ alpha"))
.expect("alpha row");
let start = text_position_for_visual_cell(&state, alpha_row, 5, width).expect("alpha start");
let end = text_position_for_visual_cell(&state, alpha_row, 10, width).expect("alpha end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("alpha".to_string())
);
}
#[test]
fn wide_code_gutter_visual_selection_copy_starts_at_visible_code_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ alpha"), "{rendered}");
let alpha_row = visual_lines(&state, width)
.iter()
.position(|line| line_text(&line.line).contains(" │ alpha"))
.expect("alpha row");
let start = text_position_for_visual_cell(&state, alpha_row, 5, width).expect("alpha start");
let end = text_position_for_visual_cell(&state, alpha_row, 10, width).expect("alpha end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("alpha".to_string())
);
}
#[test]
fn wide_assistant_literal_pipe_copy_selection_preserves_authored_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "│ literal".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ literal"), "{rendered}");
let pipe_row = visual_lines(&state, width)
.iter()
.position(|line| line_text(&line.line).contains(" │ literal"))
.expect("literal pipe row");
let start = text_position_for_visual_cell(&state, pipe_row, 3, width).expect("pipe start");
let end = text_position_for_visual_cell(&state, pipe_row, 12, width).expect("literal end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("│ literal".to_string())
);
}
#[test]
fn wide_visual_text_copy_excludes_full_card_border_glyphs() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "copy text without decoration".into(),
});
let copy_text = visual_text(&state);
assert!(copy_text.contains("User: "));
assert!(copy_text.contains("copy text without decoration"));
for border in ['┌', '┐', '└', '┘', '│', '─'] {
assert!(
!copy_text.contains(border),
"copy text leaked {border}: {copy_text}"
);
}
}
#[test]
fn compact_visual_lines_at_narrow_widths_have_no_card_border_and_copy_has_no_artifacts() {
for width in [20, 32, CARD_LAYOUT_MIN_WIDTH - 1] {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello compact copy".into(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "plain assistant answer".into(),
});
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(!rendered.contains('┌'), "width {width}");
assert!(!rendered.contains('│'), "width {width}");
assert!(!rendered.contains('─'), "width {width}");
let copy_text = visual_text_for_width(&state, width);
let copy_len = copy_text.len();
state.start_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::selection::TextPosition::new(0, 0, 0),
);
state.update_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::selection::TextPosition::new(copy_len, 0, copy_len),
);
let copied = state
.finish_selection(crate::tui::layout::TuiPane::Transcript, width)
.expect("copied text");
assert!(copied.contains("hello compact copy"));
assert!(copied.contains("plain assistant answer"));
assert!(!copied.contains('┌'));
assert!(!copied.contains('│'));
assert!(!copied.contains('─'));
}
}
#[test]
fn card_shell_is_consistent_across_widths() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello".into(),
});
state
.transcript
.push_back("tool: ⟳ bash • running".to_string());
for width in [20, CARD_LAYOUT_MIN_WIDTH, 80] {
let lines = visual_lines(&state, width);
assert!(lines.iter().any(|line| line.copy_text.starts_with("User:")));
assert!(lines.iter().all(|line| {
let text = line_text(&line.line);
UnicodeWidthStr::width(text.as_str()) == usize::from(width)
&& !text.contains(['┌', '┐', '└', '┘'])
}));
let body = lines
.iter()
.find(|line| line.copy_text.starts_with("User:"))
.unwrap();
assert!(
body.line
.spans
.iter()
.all(|span| span.style.bg == Some(MissionControlTheme::default().surface_panel()))
);
}
}
#[test]
fn top_level_card_content_uses_consistent_horizontal_padding() {
let mut user = MissionControlState::default();
user.apply_output_event(&OutputEvent::UserPrompt { text: "u".into() });
let mut assistant = MissionControlState::default();
assistant.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer".into(),
});
let read = read_card_state(
"padding-read",
ActivityStatus::Success,
serde_json::json!({"path":"r"}),
serde_json::json!({"hashline_seen_lines":[1]}),
"content",
);
let tool = linked_tool_state(
"padding-tool",
"dynamic_tool",
"dynamic_tool",
ActivityStatus::Success,
Vec::new(),
"STATUS: success",
);
for width in [22, CARD_LAYOUT_MIN_WIDTH, 80] {
for (state, marker) in [
(&user, "User: u"),
(&assistant, "answer"),
(&read, "r"),
(&tool, "STATUS: success"),
] {
let rendered = visual_lines(state, width)
.into_iter()
.map(|line| line_text(&line.line))
.find(|line| line.contains(marker))
.unwrap_or_else(|| panic!("missing {marker:?} at width {width}"));
let expected_column = if std::ptr::eq(state, &user) { 13 } else { 3 };
assert_eq!(rendered.find(marker), Some(expected_column), "{rendered:?}");
assert_eq!(
UnicodeWidthStr::width(rendered.as_str()),
usize::from(width),
"{rendered:?}"
);
assert!(rendered.ends_with(' '), "{rendered:?}");
if std::ptr::eq(state, &user) {
assert!(
rendered.starts_with(" ") && rendered.ends_with(" "),
"{rendered:?}"
);
}
}
}
for width in [1, 2] {
for state in [&user, &assistant, &read, &tool] {
assert!(!visual_lines(state, width).is_empty());
}
}
}
#[test]
fn visual_text_position_maps_inline_user_body_to_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let (row, rendered) = visual_lines(&state, width)
.into_iter()
.enumerate()
.map(|(row, line)| (row, line_text(&line.line)))
.find(|(_, line)| line.contains("User: hello"))
.expect("inline user body");
let hello = rendered.find("hello").expect("message start");
let start = text_position_for_visual_cell(&state, row, hello, width).expect("body start");
let end =
text_position_for_visual_cell(&state, row, hello + "hello".len(), width).expect("body end");
assert_eq!(
state.transcript_visual_text().get(start.byte..end.byte),
Some("hello")
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("hello".to_string())
);
}
#[test]
fn width_one_selection_maps_oversized_grapheme_and_highlights_replacement() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt { text: "界".into() });
let width = 1;
let (row, column) = visual_lines(&state, width)
.into_iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
rendered.find('�').map(|column| (row, column))
})
.expect("width-one replacement cell");
let start = text_position_for_visual_cell(&state, row, column, width).expect("cell start");
let end = text_position_for_visual_cell(&state, row, column + 1, width).expect("cell end");
let copy_text = visual_text_for_width(&state, width);
assert_eq!(copy_text.get(start.byte..end.byte), Some("界"));
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
let selected = crate::tui::transcript_projection::visible_transcript_lines(&state, width, 40);
let replacement = selected
.lines
.iter()
.flat_map(|line| line.spans.iter())
.find(|span| span.content.as_ref() == "�")
.expect("selected replacement cell");
assert_eq!(
replacement.style,
MissionControlTheme::default().selection()
);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("界".to_string())
);
}
#[test]
fn pointer_selection_keeps_leading_and_trailing_zero_width_graphemes() {
for (input, expected) in [("\u{0301}a", "\u{0301}a"), ("a\u{200b}", "a\u{200b}")] {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: input.to_string(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let lines = visual_lines(&state, width);
let (row, column) = lines
.iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
let a_byte = rendered.find('a')?;
Some((row, UnicodeWidthStr::width(&rendered[..a_byte])))
})
.expect("visible grapheme");
let start = text_position_for_visual_cell(&state, row, column, width)
.expect("visible grapheme start");
let end = text_position_for_visual_cell(&state, row, column + 1, width)
.expect("visible grapheme end");
assert_eq!(
visual_text_for_width(&state, width).get(start.byte..end.byte),
Some(expected),
"input={input:?}"
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some(expected.to_string()),
"input={input:?}"
);
}
}
#[test]
fn pointer_selection_keeps_wrapped_leading_zero_width_with_wide_grapheme() {
let mut state = MissionControlState::default();
let expected = "\u{0301}界";
state.transcript.push_back(format!("you: {expected}"));
let width = 9;
let lines = visual_lines(&state, width);
let (row, column) = lines
.iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
let byte = rendered.find('界')?;
Some((row, UnicodeWidthStr::width(&rendered[..byte])))
})
.expect("wrapped visible grapheme");
assert!(row > 0);
assert!(!line_text(&lines[row - 1].line).contains('\u{0301}'));
let start =
text_position_for_visual_cell(&state, row, column, width).expect("visible grapheme start");
let end =
text_position_for_visual_cell(&state, row, column + UnicodeWidthStr::width("界"), width)
.expect("visible grapheme end");
assert_eq!(
visual_text_for_width(&state, width).get(start.byte..end.byte),
Some(expected)
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some(expected.to_string())
);
}
#[test]
fn reverse_highlight_keeps_wrapped_leading_zero_width_with_wide_grapheme() {
let mut state = MissionControlState::default();
let expected = "\u{0301}界";
state.transcript.push_back(format!("you: {expected}"));
let width = 9;
let lines = visual_lines(&state, width);
let (label_row, label_column) = lines
.iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
let byte = rendered.find("User: ")?;
Some((row, UnicodeWidthStr::width(&rendered[..byte])))
})
.expect("user label");
let (grapheme_row, grapheme_column) = lines
.iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
let byte = rendered.find('界')?;
Some((row, UnicodeWidthStr::width(&rendered[..byte])))
})
.expect("visible grapheme");
let label_start =
text_position_for_visual_cell(&state, label_row, label_column, width).expect("label start");
let grapheme_end = text_position_for_visual_cell(
&state,
grapheme_row,
grapheme_column + UnicodeWidthStr::width("界"),
width,
)
.expect("visible grapheme end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, grapheme_end);
state.update_selection(crate::tui::layout::TuiPane::Transcript, label_start);
let selected = crate::tui::transcript_projection::visible_transcript_lines(&state, width, 20);
let selected_grapheme = selected
.lines
.iter()
.flat_map(|line| line.spans.iter())
.find(|span| {
span.content.as_ref().contains('界')
&& span.style == MissionControlTheme::default().selection()
})
.expect("reverse-selected grapheme");
assert!(selected_grapheme.content.as_ref().contains('界'));
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some(format!("User: {expected}"))
);
}
#[test]
fn pointer_selection_keeps_leading_zero_width_with_oversized_replacement() {
let mut state = MissionControlState::default();
let expected = "\u{0301}界";
state.transcript.push_back(format!("you: {expected}"));
let width = 3;
let (row, column) = visual_lines(&state, width)
.into_iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
rendered
.find('�')
.map(|byte| (row, UnicodeWidthStr::width(&rendered[..byte])))
})
.expect("replacement cell");
let start = text_position_for_visual_cell(&state, row, column, width).expect("cell start");
let end = text_position_for_visual_cell(&state, row, column + 1, width).expect("cell end");
assert_eq!(
visual_text_for_width(&state, width).get(start.byte..end.byte),
Some(expected)
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
let selected = crate::tui::transcript_projection::visible_transcript_lines(&state, width, 20);
assert!(
selected
.lines
.iter()
.flat_map(|line| line.spans.iter())
.any(|span| {
span.content.as_ref().contains('�')
&& span.style == MissionControlTheme::default().selection()
})
);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some(expected.to_string())
);
}
#[test]
fn pointer_selection_keeps_trailing_zero_width_with_full_row_grapheme() {
let mut state = MissionControlState::default();
let expected = "a\u{200b}";
state.transcript.push_back("you: a\u{200b}界".to_string());
let width = 9;
let lines = visual_lines(&state, width);
let (row, column) = lines
.iter()
.enumerate()
.find_map(|(row, line)| {
let rendered = line_text(&line.line);
let byte = rendered.find('a')?;
Some((row, UnicodeWidthStr::width(&rendered[..byte])))
})
.expect("visible grapheme");
assert!(!line_text(&lines[row + 1].line).contains('\u{200b}'));
let start = text_position_for_visual_cell(&state, row, column, width).expect("cell start");
let end = text_position_for_visual_cell(&state, row, column + 1, width).expect("cell end");
assert_eq!(
visual_text_for_width(&state, width).get(start.byte..end.byte),
Some(expected)
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some(expected.to_string())
);
}