use super::*;
#[test]
fn render_transcript_shows_streaming_assistant_label_until_complete() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "working".to_string(),
});
let backend = TestBackend::new(80, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
assert!(find_text(buffer, "🤖").is_some());
assert!(find_text(buffer, "Assistant …").is_some());
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "working".to_string(),
});
terminal.draw(|frame| draw(frame, &state)).unwrap();
let completed = terminal.backend().buffer();
assert!(find_text(completed, "🤖").is_some());
assert!(find_text(completed, "Assistant").is_some());
assert!(find_text(completed, "Assistant …").is_none());
}
#[test]
fn render_transcript_shows_context_usage_on_right_border() {
let mut state = MissionControlState::default();
for line in 0..40 {
state.apply_output_event(&OutputEvent::UserPrompt {
text: format!("line-{line}"),
});
}
state.apply_output_event(&OutputEvent::ContextUsage {
current_tokens: 16274,
max_tokens: 272000,
reasoning_tokens: Some(0),
source: crate::output::ContextUsageSource::ProviderExact,
request_sequence: 0,
});
let backend = TestBackend::new(160, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let layout = layout_for(Rect::new(0, 0, 160, 20), &state);
let rendered_transcript = rendered_text_in_area(buffer, layout.transcript);
let transcript = find_text(buffer, "Session:").unwrap();
let usage = find_text(buffer, "Context 5%/272k • latest")
.unwrap_or_else(|| panic!("usage label missing from transcript: {rendered_transcript}"));
let latest = find_text(buffer, "latest").unwrap();
assert_eq!(transcript.1, layout.transcript.y);
assert_eq!(usage.1, layout.transcript.y);
assert_eq!(latest.1, layout.transcript.y);
assert!(latest.0 > layout.transcript.x + layout.transcript.width / 2);
assert!(!rendered_transcript.contains("Thinking"));
}
#[test]
fn render_transcript_shows_scroll_position_on_top_right_title_strip() {
let mut state = MissionControlState::default();
for line in 0..12 {
state.apply_output_event(&OutputEvent::UserPrompt {
text: format!("line-{line}"),
});
}
let backend = TestBackend::new(100, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let layout = layout_for(Rect::new(0, 0, 100, 20), &state);
let title = find_text(buffer, "Session:").unwrap();
let latest = find_text(buffer, "latest").unwrap();
assert_eq!(title.1, layout.transcript.y);
assert!(title.0 < layout.transcript.x + layout.transcript.width / 2);
assert_eq!(latest.1, layout.transcript.y);
assert!(latest.0 > layout.transcript.x + layout.transcript.width / 2);
state.set_scroll_offset(&state.scroll_views.transcript, 3);
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let scrolled = find_text(buffer, "3 lines above latest").unwrap();
assert_eq!(scrolled.1, layout.transcript.y);
assert!(scrolled.0 > layout.transcript.x + layout.transcript.width / 2);
}
#[test]
fn render_places_activity_tree_above_detail_with_wider_transcript() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "hello".to_string(),
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("a1"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("bash echo"),
});
let area = Rect::new(0, 0, 100, 30);
let layout = layout_for(area, &state);
assert_eq!(
state.layout_mode,
crate::tui::state::TuiLayoutMode::TranscriptFocused
);
assert!(layout.transcript.width > layout.activity_tree.width);
assert_eq!(layout.activity_tree.width, layout.detail.width);
assert_eq!(layout.activity_tree.x, layout.detail.x);
assert!(layout.activity_tree.y < layout.detail.y);
let backend = TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let transcript = find_text(buffer, "Session:").unwrap();
let tree = find_text(buffer, "Activity Tree").unwrap();
let detail = find_text(buffer, "Selected Activity Detail").unwrap();
assert!(tree.0 <= detail.0 + 1);
assert!(tree.1 < detail.1);
assert!(transcript.0 < tree.0);
assert!(tree.0 >= 60);
}
#[test]
fn render_transcript_tails_overflow_by_default() {
let mut state = MissionControlState::default();
for line in 0..8 {
state.apply_output_event(&OutputEvent::UserPrompt {
text: format!("line-{line}"),
});
}
let backend = TestBackend::new(80, 12);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("line-7"));
assert!(!rendered.contains("line-0"));
}
#[test]
fn render_transcript_tails_single_long_wrapped_message() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: format!("HEAD {}\nTAIL-WRAPPED", "x".repeat(180)),
});
let backend = TestBackend::new(50, 12);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("TAIL-WRAPPED"));
assert!(!rendered.contains("HEAD"));
}
#[test]
fn render_transcript_tails_word_wrapped_overflow() {
let state = MissionControlState {
transcript: vec![
"assistant: alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega SENTINEL".to_string(),
]
.into(),
..Default::default()
};
let backend = TestBackend::new(33, 12);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("SENTINEL"), "{rendered}");
assert!(!rendered.contains("alpha"), "{rendered}");
}
#[test]
fn render_transcript_markdown_code_and_boundaries() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "show code".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "# Plan\n\n- use `render`\n\n```rust\nfn main() {}\n```".to_string(),
});
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "tool-1".to_string(),
name: "bash".to_string(),
arguments: serde_json::json!({}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "bash".to_string(),
success: true,
content: "ok".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(ToolDisplaySummary {
tool_name: "bash".to_string(),
status: ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "bash echo ok".to_string(),
metadata: Vec::new(),
}),
});
let backend = TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let rendered = buffer
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(find_text(buffer, "👤").is_some());
assert!(find_text(buffer, "You").is_some());
assert!(find_text(buffer, "🤖").is_some());
assert!(find_text(buffer, "Assistant").is_some());
assert!(rendered.contains("bash echo ok"));
assert!(rendered.contains("# Plan"));
assert!(rendered.contains("• use "));
assert!(rendered.contains("language: rust"));
assert!(rendered.contains("│ fn main"));
let code = find_text(buffer, "fn main").unwrap();
assert_eq!(buffer[code].bg, MATRIX_GREEN.panel);
let heading = find_text(buffer, "# Plan").unwrap();
assert_eq!(
buffer[heading].style().fg,
MATRIX_GREEN
.display_role(crate::rendering::DisplayRole::Heading)
.fg
);
}
#[test]
fn render_transcript_assistant_diff_fence_uses_diff_styles() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```diff\n-old\n+new\n```".to_string(),
});
let backend = TestBackend::new(80, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
assert!(find_text(buffer, "language: diff").is_some());
let removed = find_text(buffer, "-old").unwrap();
assert_eq!(buffer[removed].fg, MATRIX_GREEN.lavender_cool);
assert_eq!(buffer[removed].bg, MATRIX_GREEN.error_bg);
let inserted = find_text(buffer, "+new").unwrap();
assert_eq!(buffer[inserted].fg, MATRIX_GREEN.green_neon);
assert_eq!(buffer[inserted].bg, MATRIX_GREEN.success_bg);
}
#[test]
fn render_transcript_multiline_markdown_preserves_alignment() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "First paragraph\n\n- one\n- two\n\nAfter".to_string(),
});
let backend = TestBackend::new(80, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let layout = layout_for(Rect::new(0, 0, 80, 20), &state);
let rendered = rendered_text_in_area(terminal.backend().buffer(), layout.transcript);
assert!(rendered.contains(" First paragraph"), "{rendered}");
assert!(rendered.contains(" • one"), "{rendered}");
assert!(rendered.contains(" • two"), "{rendered}");
assert!(rendered.contains(" After"), "{rendered}");
}
#[test]
fn render_transcript_adds_muted_separator_before_later_user_turns() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first prompt".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer".to_string(),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "second prompt".to_string(),
});
let backend = TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let layout = layout_for(Rect::new(0, 0, 100, 30), &state);
let rendered = rendered_text_in_area(buffer, layout.transcript);
assert_eq!(rendered.matches("user turn").count(), 1, "{rendered}");
let first_prompt = find_text(buffer, "first prompt").unwrap();
let separator = find_text(buffer, "user turn").unwrap();
let second_prompt = find_text(buffer, "second prompt").unwrap();
assert!(first_prompt.1 < separator.1, "{rendered}");
assert!(separator.1 < second_prompt.1, "{rendered}");
assert_eq!(buffer[separator].fg, MATRIX_GREEN.green_dim);
assert!(buffer[separator].modifier.contains(Modifier::DIM));
}
fn render_transcript_scrollbar_case(transcript: Vec<String>) -> (Buffer, Rect) {
let state = MissionControlState {
transcript: transcript.into(),
..Default::default()
};
let area = Rect::new(0, 0, 80, 20);
let layout = layout_for(area, &state);
let backend = TestBackend::new(area.width, area.height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
(
terminal.backend().buffer().clone(),
transcript_text_area_for_pane(layout.transcript),
)
}
fn assert_transcript_scrollbar_gutter(
buffer: &Buffer,
text_area: Rect,
adjacent_row_text: &str,
expected_adjacent: &str,
) {
let gutter_x = text_area.x + text_area.width - 1;
let adjacent_row = find_text_in_area(buffer, text_area, adjacent_row_text)
.unwrap_or_else(|| panic!("transcript fixture text missing: {adjacent_row_text}"))
.1;
let thumb = (gutter_x, adjacent_row);
assert_eq!(buffer[thumb].symbol(), "┃");
assert_eq!(buffer[thumb].fg, MATRIX_GREEN.green_bright);
assert_eq!(buffer[thumb].bg, ratatui::style::Color::Reset);
assert_eq!(
buffer[(gutter_x - 1, adjacent_row)].symbol(),
expected_adjacent,
);
}
#[test]
fn production_transcript_scrollbar_is_visible_without_overflow() {
let fixture = format!("fit-sentinel{}", "F".repeat(100));
let (buffer, text_area) = render_transcript_scrollbar_case(vec![fixture]);
assert_transcript_scrollbar_gutter(&buffer, text_area, "fit-sentinel", "F");
}
#[test]
fn production_transcript_scrollbar_is_visible_with_overflow() {
let (buffer, text_area) = render_transcript_scrollbar_case(
(0..40)
.map(|line| format!("overflow-line-{line}{}", "O".repeat(60)))
.collect(),
);
assert_transcript_scrollbar_gutter(&buffer, text_area, "overflow-line-39", "O");
assert!(find_text_in_area(&buffer, text_area, "overflow-line-0").is_none());
}