use super::*;
#[test]
fn transcript_timestamps_render_inline_and_stay_out_of_copy() {
let local_timestamp =
chrono::TimeZone::with_ymd_and_hms(&chrono::Local, 2024, 1, 2, 3, 4, 5).unwrap();
let timestamp = local_timestamp.with_timezone(&chrono::Utc);
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer".into(),
});
state.set_transcript_timestamp_for_test(0, local_timestamp);
let lines = visual_lines(&state, 24);
let header = line_text(&lines[1].line);
assert_eq!(UnicodeWidthStr::width(header.as_str()), 24);
assert!(header.ends_with(" 03:04:05"), "{header:?}");
assert_eq!(lines[1].copy_text, "Agent");
assert!(!state.transcript_visual_text().contains("03:04:05"));
let mut user = MissionControlState::default();
user.apply_output_event_at(
&OutputEvent::UserPrompt {
text: "hello".into(),
},
Some(timestamp),
);
let user_lines = visual_lines(&user, 26);
assert_eq!(line_text(&user_lines[1].line), " 03:04:05 | User: hello ");
assert_eq!(user_lines[1].copy_text, "User: hello");
assert!(user_lines[1].copyable);
assert!(!user_lines[0].copyable);
assert!(!user_lines[2].copyable);
assert_eq!(user_lines[0].copy_text, "");
assert_eq!(user_lines[2].copy_text, "");
assert!(!user.transcript_visual_text().contains("03:04:05"));
assert!(
user_lines[1]
.line
.spans
.iter()
.all(|span| { span.style.bg == Some(MissionControlTheme::default().surface_panel()) })
);
let timestamp_span = user_lines[1]
.line
.spans
.iter()
.find(|span| span.content.as_ref() == "03:04:05")
.expect("inline timestamp span");
assert_eq!(
timestamp_span.style.fg,
Some(MissionControlTheme::default().text_muted())
);
assert!(timestamp_span.style.add_modifier.contains(Modifier::DIM));
let user_start = line_text(&user_lines[1].line)
.find("User: ")
.expect("user label");
let timestamp_start = line_text(&user_lines[1].line)
.find("03:04:05")
.expect("timestamp");
for column in timestamp_start..user_start {
let start = text_position_for_visual_cell(&user, 1, column, 26).expect("prefix start");
let end = text_position_for_visual_cell(&user, 1, column + 1, 26).expect("prefix end");
assert_eq!(start.byte, end.byte, "column={column}");
}
let start = text_position_for_visual_cell(&user, 1, user_start, 26).expect("user start");
let end = text_position_for_visual_cell(&user, 1, 26, 26).expect("user end");
user.start_selection(crate::tui::layout::TuiPane::Transcript, start);
user.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
user.finish_selection(crate::tui::layout::TuiPane::Transcript, 26),
Some("User: hello".to_string())
);
}
#[test]
fn user_prompt_without_timestamp_has_no_dangling_separator() {
let mut state = MissionControlState::default();
state.transcript.push_back("you: hello".to_string());
let lines = visual_lines(&state, 24);
assert_eq!(lines[1].copy_text, "User: hello");
assert_eq!(line_text(&lines[1].line), " User: hello ");
assert!(!line_text(&lines[1].line).contains(" | "));
assert_eq!(visual_text_for_width(&state, 24), "User: hello");
}
#[test]
fn user_prompt_multiline_and_empty_copy_keep_logical_newlines() {
let mut state = MissionControlState::default();
state
.transcript
.push_back("you: first\n\nthird".to_string());
assert_eq!(visual_text_for_width(&state, 24), "User: first\n\nthird");
let mut empty = MissionControlState::default();
empty.transcript.push_back("you: ".to_string());
let lines = visual_lines(&empty, 24);
assert_eq!(lines[1].copy_text, "User: ");
assert_eq!(visual_text_for_width(&empty, 24), "User: ");
}
#[test]
fn user_prompt_inline_prefix_wraps_and_keeps_tiny_rows_full_width() {
let timestamp = chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2024, 1, 2, 3, 4, 5).unwrap();
let mut state = MissionControlState::default();
state.apply_output_event_at(
&OutputEvent::UserPrompt {
text: "abc界".into(),
},
Some(timestamp),
);
for width in [1, 2, 3, 12, 24] {
let lines = visual_lines(&state, width);
assert!(
lines.iter().all(|line| {
UnicodeWidthStr::width(line_text(&line.line).as_str()) == usize::from(width)
}),
"width={width}: {lines:?}"
);
assert_eq!(
visual_text_for_width(&state, width),
"User: abc界",
"width={width}"
);
assert!(lines.iter().all(|line| visual_line_rows(line, width) == 1));
}
}
#[test]
fn transcript_timestamp_headers_fit_narrow_widths_without_wrapping() {
let local_timestamp =
chrono::TimeZone::with_ymd_and_hms(&chrono::Local, 2024, 1, 2, 3, 4, 5).unwrap();
let timestamp = local_timestamp.with_timezone(&chrono::Utc);
let mut state = MissionControlState::default();
state.apply_output_event_at(
&OutputEvent::AssistantComplete {
text: "answer".into(),
},
Some(timestamp),
);
for width in 1..=12 {
let lines = visual_lines(&state, width);
assert!(
lines[..3]
.iter()
.all(|line| visual_line_rows(line, width) == 1),
"width={width}"
);
assert!(lines.iter().all(|line| {
UnicodeWidthStr::width(line_text(&line.line).as_str()) == usize::from(width)
}));
}
}
#[test]
fn transcript_timestamp_copy_mapping_collapses_tiny_timestamp_headers() {
let mut state = timestamped_long_header_state();
for width in 1..=8 {
let lines = visual_lines(&state, width);
assert!(
lines[1].copy_text.is_empty(),
"width={width}: {:?}",
lines[1]
);
for column in 0..usize::from(width) {
let start = text_position_for_visual_cell(&state, 1, column, width)
.expect("timestamp cell start");
let end = text_position_for_visual_cell(&state, 1, column + 1, width)
.expect("timestamp cell end");
assert_eq!(start.byte, end.byte, "width={width} column={column}");
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),
None,
"width={width} column={column}"
);
}
}
}
#[test]
fn transcript_timestamp_copy_mapping_collapses_trailing_region_and_hidden_title() {
let mut state = timestamped_long_header_state();
let width = 24;
let lines = visual_lines(&state, width);
let header_copy = lines[1].copy_text.clone();
assert_eq!(header_copy, "✓ Tool • very-");
assert!(!header_copy.contains("hidden-tail"));
let rendered = line_text(&lines[1].line);
assert!(rendered.ends_with(" 03:04:05"), "{rendered:?}");
let timestamp_start = usize::from(width) - "03:04:05".len();
for start_column in [timestamp_start - 1, timestamp_start] {
let start = text_position_for_visual_cell(&state, 1, start_column, width)
.expect("trailing-region start");
let end = text_position_for_visual_cell(&state, 1, usize::from(width), width)
.expect("timestamp end");
assert_eq!(start.byte, end.byte, "start_column={start_column}");
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),
None,
"start_column={start_column}"
);
}
let start = text_position_for_visual_cell(&state, 1, 1, width).expect("header start");
let end =
text_position_for_visual_cell(&state, 1, usize::from(width), width).expect("header end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
let copied = state
.finish_selection(crate::tui::layout::TuiPane::Transcript, width)
.expect("visible header copy");
assert_eq!(copied, header_copy);
assert!(!copied.contains("hidden-tail"));
}