use super::*;
use ratatui::Terminal;
#[test]
fn summary_tab_spinner_sits_above_label_and_tracks_summary_work() {
let mut state = MissionControlState::default();
let mut terminal = Terminal::new(TestBackend::new(5, 24)).unwrap();
for tab in [RightColumnTab::Activity, RightColumnTab::Summary] {
state.right_column_tab = tab;
for running in [false, true] {
state.summary.running = running;
state.activity_motion.phase = 3;
terminal
.draw(|frame| draw_right_column_rail(frame, frame.area(), &state))
.unwrap();
let buffer = terminal.backend().buffer();
let summary_y = (1..24).find(|&y| buffer[(2, y)].symbol() == "S").unwrap();
let spinner = &buffer[(2, summary_y - 1)];
assert_eq!(spinner.symbol(), if running { "⠸" } else { "⠋" });
assert_eq!(buffer[(2, summary_y - 2)].symbol(), " ");
assert_eq!(
spinner.fg,
if running {
state.theme.text_accent()
} else {
state.theme.text_muted()
}
);
assert_eq!(buffer[(2, summary_y + 1)].symbol(), "u");
}
}
}
#[test]
fn modal_helpers_preserve_existing_centered_geometry() {
let area = Rect::new(0, 0, 100, 30);
assert_eq!(
modal_rect(area, SYSTEM_PROMPT_MODAL),
Some(Rect::new(5, 3, 90, 24))
);
assert_eq!(system_prompt_text_area(area), Some(Rect::new(7, 6, 86, 19)));
assert_eq!(
sessions_modal_list_area(area),
Some(Rect::new(8, 7, 28, 17))
);
assert_eq!(
sessions_modal_preview_area(area),
Some(Rect::new(39, 7, 53, 17))
);
assert_eq!(
model_picker_list_area(area, true),
Some(Rect::new(12, 10, 76, 12))
);
assert_eq!(modal_rect(Rect::new(0, 0, 10, 4), PICKER_MODAL), None);
}
#[test]
fn logout_confirmation_action_row_tracks_complete_target_geometry() {
let label = "OpenAI Codex";
let provider_id = "openai-codex";
assert_eq!(
logout_confirmation_action_row_area(Rect::new(0, 0, 11, 4), label, provider_id),
None
);
assert_eq!(
logout_confirmation_action_row_area(Rect::new(0, 0, 12, 7), label, provider_id),
None
);
assert_eq!(
logout_confirmation_action_row_area(Rect::new(0, 0, 89, 7), label, provider_id),
None
);
assert_eq!(
logout_confirmation_action_row_area(Rect::new(0, 0, 90, 7), label, provider_id),
Some(Rect::new(20, 4, 50, 1))
);
assert_eq!(
logout_confirmation_action_row_area(
Rect::new(0, 0, 90, 7),
"A very long provider label that wraps",
"a-provider-id-that-also-wraps",
),
None
);
}
#[test]
fn render_has_header_transcript_tree_detail_prompt_and_help() {
let mut state = MissionControlState {
status: "ready".to_string(),
..Default::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 backend = TestBackend::new(100, 30);
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(&format!("MAGI-CODE v{}", env!("CARGO_PKG_VERSION"))));
assert!(!rendered.contains("bash preview"));
assert!(!rendered.contains("Header"));
assert!(!rendered.contains("Mission Control"));
assert!(rendered.contains("Session:"));
assert!(rendered.contains("[Alt-A] Show/Hide Panel"));
assert!(rendered.contains("Activity Tree"));
assert!(!rendered.contains("hide right column"));
assert!(rendered.contains("Selected Activity Detail"));
assert!(rendered.contains("Session: 0(in)"));
assert!(!rendered.contains("Prompt [Alt-P]"));
state.show_help = true;
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered_with_help = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered_with_help.contains("Help"));
}
#[test]
fn hidden_activity_column_renders_only_full_width_transcript() {
let mut state = MissionControlState::default();
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("hidden-activity"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("hidden activity label"),
});
state.toggle_activity_column();
let area = Rect::new(0, 0, 100, 30);
let layout = layout_for(area, &state);
assert_eq!(layout.transcript.width, area.width);
assert_eq!(layout.activity_tree, None);
assert_eq!(layout.detail, None);
let backend = TestBackend::new(area.width, area.height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(rendered.contains("[Alt-A] Show/Hide Panel"), "{rendered}");
assert!(!rendered.contains("Activity Tree"), "{rendered}");
assert!(!rendered.contains("Selected Activity Detail"), "{rendered}");
assert!(!rendered.contains("hidden activity label"), "{rendered}");
}
#[test]
fn default_right_column_starts_on_activity_with_tree_and_detail() {
let state = MissionControlState::default();
let area = Rect::new(0, 0, 100, 30);
let layout = layout_for(area, &state);
assert_eq!(state.right_column_tab, RightColumnTab::Activity);
assert!(layout.activity_tree.is_some());
assert!(layout.detail.is_some());
assert_eq!(layout.summary, None);
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(rendered.contains("Activity Tree"), "{rendered}");
assert!(rendered.contains("Selected Activity Detail"), "{rendered}");
assert!(
!rendered.contains("Summary functionality coming soon"),
"{rendered}"
);
}
#[test]
fn summary_renders_controls_status_errors_and_log_without_activity_panes() {
let mut state = MissionControlState::default();
state.focus_summary();
state.summary.enabled = true;
state.summary.entries = vec!["Implemented session controls".into()];
state.summary.error = Some("Provider unavailable".into());
let area = Rect::new(0, 0, 100, 30);
let layout = layout_for(area, &state);
assert_eq!(layout.activity_tree, None);
assert_eq!(layout.detail, None);
let summary = layout.summary.expect("summary pane");
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), summary);
for text in [
"[ Stop ]",
"Waiting",
"Provider unavailable",
"Implemented session controls",
] {
assert!(rendered.contains(text), "{rendered}");
}
assert!(!rendered.contains("[Alt-A]"), "{rendered}");
let whole_screen = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(!whole_screen.contains("Activity Tree"), "{whole_screen}");
assert!(
!whole_screen.contains("Selected Activity Detail"),
"{whole_screen}"
);
assert!(
whole_screen.contains("[Alt-A] Show/Hide Panel"),
"{whole_screen}"
);
}
#[test]
fn summary_shows_latest_entry_and_scrollbar_after_new_entries() {
let mut state = MissionControlState::default();
let area = Rect::new(0, 0, 40, 30);
state.summary.apply(crate::summarizer::SummarySnapshot {
session_id: "summary-render".into(),
enabled: true,
running: false,
entries: (1..=20)
.map(|index| format!("Checkpoint {index}"))
.collect(),
error: None,
});
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal
.draw(|frame| draw_summary(frame, area, &state))
.unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(rendered.contains("Entry 20 · Latest"), "{rendered}");
assert!(rendered.contains("Checkpoint 20"), "{rendered}");
assert!(!rendered.contains("── Entry 1 ──"), "{rendered}");
let text_area = pane_text_area(
themed_block_with_theme(state.theme, "Summary", false).inner(summary_sections(area)[0]),
);
let scrollbar_x = text_area.right() - 1;
assert_eq!(
terminal.backend().buffer()[(scrollbar_x, text_area.bottom() - 1)].symbol(),
"↓"
);
assert_eq!(
terminal.backend().buffer()[(scrollbar_x, text_area.bottom() - 2)].symbol(),
"┃"
);
state.summary.scroll = 0;
terminal
.draw(|frame| draw_summary(frame, area, &state))
.unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(rendered.contains("── Entry 1 ──"), "{rendered}");
assert!(!rendered.contains("Checkpoint 20"), "{rendered}");
assert_eq!(
terminal.backend().buffer()[(scrollbar_x, text_area.y + 2)].symbol(),
"↑"
);
assert_eq!(
terminal.backend().buffer()[(scrollbar_x, text_area.y + 3)].symbol(),
"┃"
);
}
#[test]
fn right_column_rail_is_far_right_and_does_not_overlap_content() {
let area = Rect::new(7, 4, 100, 30);
let state = MissionControlState::default();
let layout = layout_for(area, &state);
let rail = layout.right_column_rail.expect("right-column rail");
let tree = layout.activity_tree.expect("activity tree");
let detail = layout.detail.expect("activity detail");
let right = |rect: Rect| rect.x.saturating_add(rect.width);
assert_eq!(right(rail), right(area));
assert_eq!(right(tree), rail.x);
assert_eq!(right(detail), rail.x);
assert_eq!(tree.x, detail.x);
assert_eq!(
tree.x,
layout.transcript.x.saturating_add(layout.transcript.width)
);
}
#[test]
fn right_column_rail_tabs_have_horizontal_and_vertical_padding() {
let area = Rect::new(0, 0, 100, 30);
let mut state = MissionControlState::default();
let mut tab_areas = None;
for selected in [RightColumnTab::Activity, RightColumnTab::Summary] {
state.right_column_tab = selected;
let layout = layout_for(area, &state);
let rail = layout.right_column_rail.expect("right-column rail");
assert_eq!(rail.width, 5);
let current_areas = (
layout.right_column_tab_area(RightColumnTab::Activity),
layout.right_column_tab_area(RightColumnTab::Summary),
);
if let Some(previous) = tab_areas {
assert_eq!(current_areas, previous);
} else {
tab_areas = Some(current_areas);
}
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
for (tab, label) in [
(RightColumnTab::Activity, "Activity"),
(RightColumnTab::Summary, "Summary"),
] {
let tab_area = layout.right_column_tab_area(tab).expect("tab area");
assert_eq!(
tab_area.height,
label.chars().count() as u16 + 2 + u16::from(tab == RightColumnTab::Summary)
);
for (row, character) in label.chars().enumerate() {
let position = (
rail.x + 2,
tab_area.y + 1 + u16::from(tab == RightColumnTab::Summary) + row as u16,
);
assert_eq!(buffer[position].symbol(), character.to_string());
assert_eq!(buffer[(rail.x + 1, position.1)].symbol(), " ");
assert_eq!(buffer[(rail.x + 3, position.1)].symbol(), " ");
assert_eq!(buffer[(rail.x + 4, position.1)].symbol(), " ");
assert_eq!(
buffer[(rail.x, position.1)].symbol(),
if tab == selected { "▌" } else { " " }
);
}
for row in [tab_area.y, tab_area.y + tab_area.height - 1] {
assert_eq!(
buffer[(rail.x, row)].symbol(),
if tab == selected { "▌" } else { " " }
);
for column in (rail.x + 1)..(rail.x + rail.width) {
assert_eq!(buffer[(column, row)].symbol(), " ");
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RenderedRailStyles {
selected_tab: ratatui::style::Style,
unselected_tab: ratatui::style::Style,
separator: ratatui::style::Style,
background: ratatui::style::Style,
}
fn custom_right_column_theme(seed: u8) -> MissionControlTheme {
let appearance = crate::appearance::RuntimeAppearance::default();
let mut resolved = (*appearance.theme).clone();
let color = |red: u8, green: u8, blue: u8| {
opaline::OpalineColor::new(
seed.saturating_add(red),
seed.saturating_add(green),
seed.saturating_add(blue),
)
};
let roles = &mut resolved.roles;
roles.surface_background = color(1, 2, 3);
roles.text_primary = color(4, 5, 6);
roles.surface_selection = color(7, 8, 9);
let mut selected = opaline::OpalineStyle::new()
.with_fg(color(10, 11, 12))
.with_bg(color(13, 14, 15));
selected.underline = true;
roles.standard_styles.selected = selected;
let mut dimmed = opaline::OpalineStyle::new()
.with_fg(color(16, 17, 18))
.with_bg(color(19, 20, 21));
dimmed.italic = true;
dimmed.reversed = true;
roles.standard_styles.dimmed = dimmed;
let appearance = appearance.with_resolved_theme(resolved);
MissionControlTheme::from_runtime_appearance(&appearance, u64::from(seed))
}
fn rendered_right_column_rail_styles(
theme: MissionControlTheme,
selected: RightColumnTab,
) -> RenderedRailStyles {
let area = Rect::new(0, 0, 100, 30);
let state = MissionControlState {
right_column_tab: selected,
theme,
..Default::default()
};
let layout = layout_for(area, &state);
let rail = layout.right_column_rail.expect("right-column rail");
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let tab_position = |tab: RightColumnTab| {
let tab_area = layout.right_column_tab_area(tab).expect("right-column tab");
let label = match tab {
RightColumnTab::Activity => "A",
RightColumnTab::Summary => "S",
};
find_text_in_area(buffer, tab_area, label).expect("right-column tab label")
};
let selected_position = tab_position(selected);
let unselected_position = tab_position(match selected {
RightColumnTab::Activity => RightColumnTab::Summary,
RightColumnTab::Summary => RightColumnTab::Activity,
});
let activity_area = layout
.right_column_tab_area(RightColumnTab::Activity)
.expect("activity tab");
RenderedRailStyles {
selected_tab: buffer[selected_position].style(),
unselected_tab: buffer[unselected_position].style(),
separator: buffer[(rail.x, activity_area.y + activity_area.height)].style(),
background: buffer[(rail.x + rail.width - 1, rail.y + rail.height - 1)].style(),
}
}
fn assert_rendered_style(
actual: ratatui::style::Style,
expected: ratatui::style::Style,
label: &str,
) {
assert_eq!(actual.fg, expected.fg, "{label} foreground");
assert_eq!(actual.bg, expected.bg, "{label} background");
assert_eq!(
actual.add_modifier, expected.add_modifier,
"{label} modifiers"
);
assert_eq!(
actual.sub_modifier, expected.sub_modifier,
"{label} removed modifiers"
);
}
fn assert_right_column_rail_styles(
theme: MissionControlTheme,
selected: RightColumnTab,
rendered: RenderedRailStyles,
) {
assert_rendered_style(
rendered.selected_tab,
theme.right_column_rail_active_tab(),
&format!("selected {selected:?} tab style"),
);
assert_rendered_style(
rendered.unselected_tab,
theme.right_column_rail_inactive_tab(),
&format!("unselected {selected:?} tab style"),
);
assert_rendered_style(
rendered.separator,
theme.right_column_rail_separator(),
"separator style",
);
assert_rendered_style(
rendered.background,
theme.right_column_rail_background(),
"rail background style",
);
}
#[test]
fn right_column_rail_uses_theme_styles_for_each_runtime_theme_and_selection() {
let first_theme = custom_right_column_theme(10);
let second_theme = custom_right_column_theme(100);
let first_activity = rendered_right_column_rail_styles(first_theme, RightColumnTab::Activity);
assert_right_column_rail_styles(first_theme, RightColumnTab::Activity, first_activity);
let first_summary = rendered_right_column_rail_styles(first_theme, RightColumnTab::Summary);
assert_right_column_rail_styles(first_theme, RightColumnTab::Summary, first_summary);
let second_activity = rendered_right_column_rail_styles(second_theme, RightColumnTab::Activity);
assert_right_column_rail_styles(second_theme, RightColumnTab::Activity, second_activity);
assert_ne!(
first_activity.selected_tab, first_activity.unselected_tab,
"active and inactive tabs should render differently"
);
assert_ne!(
first_activity.selected_tab, first_summary.unselected_tab,
"changing the selected tab should change the style at the selected label"
);
assert_ne!(
first_activity.selected_tab, second_activity.selected_tab,
"runtime theme should change the active tab style"
);
assert_ne!(
first_activity.unselected_tab, second_activity.unselected_tab,
"runtime theme should change the inactive tab style"
);
assert_ne!(
first_activity.separator, second_activity.separator,
"runtime theme should change the separator style"
);
assert_ne!(
first_activity.background, second_activity.background,
"runtime theme should change the rail background style"
);
}
#[test]
fn panel_toggle_is_right_aligned_in_transcript_header_in_both_states() {
const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef-0123456789abcdef";
const PANEL_TOGGLE: &str = "[Alt-A] Show/Hide Panel";
for width in [44, 60, 80] {
let area = Rect::new(0, 0, width, 30);
let mut state = MissionControlState {
footer_session: SESSION_ID.to_string(),
..Default::default()
};
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("narrow-title-activity"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("narrow title activity"),
});
for column_visible in [true, false] {
if state.activity_column_visible() != column_visible {
state.toggle_activity_column();
}
let backend = TestBackend::new(area.width, area.height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let layout = layout_for(area, &state);
let title_area = transcript_title_area_for_pane(layout.transcript);
let hotkey = find_text_in_area(buffer, title_area, PANEL_TOGGLE)
.unwrap_or_else(|| panic!("missing panel toggle at width {width}"));
assert_eq!(
hotkey.0 + PANEL_TOGGLE.len() as u16,
title_area.x + title_area.width
);
assert_eq!(
layout.pane_at(hotkey.0, hotkey.1),
Some(TuiPane::Transcript)
);
assert_eq!(buffer[hotkey].fg, MissionControlTheme::default().text_dim());
if title_area.width >= PANEL_TOGGLE.len() as u16 + " Session:".len() as u16 {
assert!(
find_text_in_area(buffer, title_area, "Session:").is_some(),
"title lost session label at width {width}"
);
}
if let Some(activity) = layout.activity_tree {
assert!(
find_text_in_area(buffer, activity, "[Alt-A]").is_none(),
"activity title retained panel toggle at width {width}"
);
}
}
}
}
#[test]
fn duplicate_session_headers_render_as_one_transcript_card() {
let mut state = MissionControlState::default();
for cwd in ["/repo/first", "/repo/duplicate"] {
state.apply_output_event(&OutputEvent::SessionHeader {
session_id: Some("session-a".to_string()),
model: "gpt-5".to_string(),
cwd: std::path::PathBuf::from(cwd),
});
}
let backend = TestBackend::new(100, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), Rect::new(0, 0, 100, 20));
println!("{rendered}");
assert_eq!(rendered.matches("gpt-5").count(), 1, "{rendered}");
assert!(rendered.contains("/repo/first"), "{rendered}");
assert!(!rendered.contains("/repo/duplicate"), "{rendered}");
}
#[test]
fn render_body_starts_with_transcript_title_strip() {
let state = MissionControlState {
footer_session: "session-a".to_string(),
footer_cwd: "/repo".to_string(),
footer_git_branch: Some("main".to_string()),
transcript: vec!["transcript body".to_string()].into(),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
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 session = find_text(buffer, "Session: session-a").unwrap();
let layout = layout_for(Rect::new(0, 0, 100, 20), &state);
assert_eq!(layout.transcript.y, 0);
let top_row = (layout.transcript.x..layout.transcript.x + layout.transcript.width)
.map(|x| buffer[(x, layout.transcript.y)].symbol())
.collect::<String>();
assert_eq!(
top_row,
" ".repeat(layout.transcript.width as usize),
"transcript top padding row is not blank"
);
assert_eq!(session.1, layout.transcript.y + 1);
let workspace = find_text(buffer, "Branch: main").unwrap();
assert_eq!(workspace, (session.0, session.1 + 1));
assert_eq!(Some(buffer[workspace].fg), state.theme.muted(false).fg);
let lower_padding_row = (layout.transcript.x..layout.transcript.x + layout.transcript.width)
.map(|x| buffer[(x, layout.transcript.y + 3)].symbol())
.collect::<String>();
assert_eq!(
lower_padding_row,
" ".repeat(layout.transcript.width as usize),
"row below the workspace header is not blank"
);
assert!(find_text(buffer, "Transcript ").is_none());
assert!(find_text(buffer, "MAGI-CODE").is_some());
assert_ne!(
buffer[(layout.transcript.x, layout.transcript.y)].symbol(),
"┌"
);
assert_ne!(
buffer[(layout.transcript.x, layout.transcript.y)].symbol(),
"└"
);
let title_area = transcript_title_area_for_pane(layout.transcript);
let text_area = transcript_text_area_for_pane(layout.transcript);
assert_eq!(
title_area,
Rect::new(1, layout.transcript.y + 1, layout.transcript.width - 2, 1)
);
assert_eq!(text_area.x, layout.transcript.x + 1);
assert_eq!(text_area.width, layout.transcript.width - 2);
assert_eq!(text_area.y, layout.transcript.y + 4);
assert_eq!(
text_area.y + text_area.height,
layout.transcript.y + layout.transcript.height
);
}
#[test]
fn transcript_geometry_reserves_workspace_row_and_blank_separator() {
let area = Rect::new(10, 20, 8, 0);
for height in 0..=6 {
let area = Rect { height, ..area };
let title = transcript_title_area_for_pane(area);
let text = transcript_text_area_for_pane(area);
assert_eq!(
title,
Rect::new(
11,
if height > 2 { 21 } else { 20 },
6,
u16::from(height > 0)
)
);
assert_eq!(
text,
Rect::new(11, 20 + height.min(4), 6, height.saturating_sub(4))
);
}
}
#[test]
fn activity_focused_layout_makes_activity_column_wider() {
let mut state = MissionControlState::default();
state.focus_activity_layout();
let layout = layout_for(Rect::new(0, 0, 100, 30), &state);
let activity_tree = layout.activity_tree.expect("activity tree");
let detail = layout.detail.expect("detail");
assert_eq!(
state.layout_mode,
crate::tui::state::TuiLayoutMode::ActivityFocused
);
assert!(activity_tree.width > layout.transcript.width);
assert_eq!(activity_tree.width, detail.width);
assert_eq!(activity_tree.x, detail.x);
assert!(activity_tree.y < detail.y);
}
#[test]
fn render_transcript_tints_non_assistant_body_text_by_role() {
let state = MissionControlState {
transcript: vec![
"you: user body".to_string(),
"assistant: assistant body".to_string(),
"tool: tool body".to_string(),
"error: error body".to_string(),
"session: session body".to_string(),
]
.into(),
..Default::default()
};
let backend = TestBackend::new(100, 45);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let user_body = find_text(buffer, "user body").unwrap();
assert_eq!(
buffer[user_body].fg,
MissionControlTheme::default().transcript_user_color()
);
assert_eq!(
buffer[user_body].bg,
MissionControlTheme::default().surface_panel()
);
assert!(!buffer[user_body].modifier.contains(Modifier::DIM));
let assistant_body = find_text(buffer, "assistant body").unwrap();
assert_eq!(
buffer[assistant_body].fg,
MissionControlTheme::default().text_primary()
);
assert_eq!(
buffer[assistant_body].bg,
MissionControlTheme::default().surface_panel()
);
let tool_body = find_text(buffer, "tool body").unwrap();
assert_eq!(
buffer[tool_body].fg,
MissionControlTheme::default().transcript_tool_color()
);
assert_eq!(
buffer[tool_body].bg,
MissionControlTheme::default().surface_panel()
);
assert!(buffer[tool_body].modifier.contains(Modifier::DIM));
let error_label = find_text(buffer, "Error").unwrap();
assert_eq!(
buffer[error_label].fg,
MissionControlTheme::default().transcript_error_label_color()
);
assert_eq!(
buffer[error_label].bg,
MissionControlTheme::default().surface_error()
);
assert!(buffer[error_label].modifier.contains(Modifier::BOLD));
let error_body = find_text(buffer, "error body").unwrap();
assert_eq!(
buffer[error_body].fg,
state.theme.transcript_error_body().fg.unwrap()
);
assert_eq!(
buffer[error_body].bg,
MissionControlTheme::default().surface_error()
);
assert!(buffer[error_body].modifier.contains(Modifier::DIM));
let session_body = find_text(buffer, "session body").unwrap();
assert_eq!(
buffer[session_body].fg,
MissionControlTheme::default().text_muted()
);
assert_eq!(
buffer[session_body].bg,
MissionControlTheme::default().surface_panel()
);
assert!(buffer[session_body].modifier.contains(Modifier::DIM));
}
#[test]
fn layout_grows_prompt_to_five_wrapped_rows() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new_with_visibility(
&"x".repeat(28),
0,
true,
),
..Default::default()
};
let layout = layout_for(Rect::new(0, 0, 20, 30), &state);
assert_eq!(layout.prompt.height, 4);
state.set_prompt_text(&"x".repeat(100), 0);
let capped_layout = layout_for(Rect::new(0, 0, 20, 30), &state);
assert_eq!(capped_layout.prompt.height, 7);
}
#[test]
fn layout_routes_mouse_coordinates_to_hovered_pane() {
let mut state = MissionControlState::default();
let layout = layout_for(Rect::new(0, 0, 100, 30), &state);
assert_eq!(layout.pane_at(1, 4), Some(TuiPane::Transcript));
assert_eq!(layout.pane_at(61, 4), Some(TuiPane::ActivityTree));
assert_eq!(layout.pane_at(61, 18), Some(TuiPane::Detail));
assert_eq!(layout.pane_at(1, 27), Some(TuiPane::Prompt));
state.show_help = true;
let layout = layout_for(Rect::new(0, 0, 100, 30), &state);
assert_eq!(layout.pane_at(50, 15), Some(TuiPane::Help));
}
#[test]
fn summary_files_split_and_small_viewports_render_safely() {
let mut state = MissionControlState::default();
state
.session_files
.record("read", true, &serde_json::json!({"path":"src/lib.rs"}), &[]);
let area = Rect::new(0, 0, 60, 30);
let sections = summary_sections(area);
assert_eq!(sections[0].height, 12);
assert_eq!(sections[1].height, 18);
let mut terminal = Terminal::new(TestBackend::new(60, 30)).unwrap();
terminal
.draw(|frame| draw_summary(frame, area, &state))
.unwrap();
let text = rendered_text_in_area(terminal.backend().buffer(), sections[1]);
assert!(text.contains("Session files"), "{text}");
assert!(text.contains("lib.rs R"), "{text}");
let body = summary_section_body(area, true);
let buffer = terminal.backend().buffer();
for x in body.x..body.right() {
assert_eq!(buffer[(x, body.y - 1)].symbol(), " ");
}
assert_eq!(buffer[(body.x, body.y)].symbol(), "└");
assert_eq!(buffer[(body.x, body.y + 1)].fg, state.theme.text_accent());
for width in 0..5 {
for height in 0..8 {
terminal
.draw(|frame| draw_summary(frame, Rect::new(0, 0, width, height), &state))
.unwrap();
}
}
}