use crate::tui::state::{
BuildStatus, CircuitState, ColorBlindMode, ConfirmDialog, Panel, TuiState, WorkerStatus,
};
use crate::ui::theme::{StatusIndicator, Symbols};
use ftui::Frame;
use ftui_core::geometry::Rect;
use ftui_layout::{Constraint, Flex};
use ftui_render::cell::PackedRgba;
use ftui_style::Style;
use ftui_text::{Line, Span, WrapMode};
use ftui_widgets::Widget;
use ftui_widgets::block::{Alignment, Block};
use ftui_widgets::borders::Borders;
use ftui_widgets::list::{List, ListItem};
use ftui_widgets::paragraph::Paragraph;
const C_WHITE: PackedRgba = PackedRgba::rgb(255, 255, 255);
const C_BLACK: PackedRgba = PackedRgba::rgb(0, 0, 0);
const C_RED: PackedRgba = PackedRgba::rgb(205, 0, 0);
const C_GREEN: PackedRgba = PackedRgba::rgb(0, 205, 0);
const C_YELLOW: PackedRgba = PackedRgba::rgb(205, 205, 0);
const C_BLUE: PackedRgba = PackedRgba::rgb(0, 0, 238);
const C_CYAN: PackedRgba = PackedRgba::rgb(0, 205, 205);
const C_DARK_GRAY: PackedRgba = PackedRgba::rgb(85, 85, 85);
const C_GRAY: PackedRgba = PackedRgba::rgb(170, 170, 170);
const C_LIGHT_GREEN: PackedRgba = PackedRgba::rgb(85, 255, 85);
const C_LIGHT_YELLOW: PackedRgba = PackedRgba::rgb(255, 255, 85);
const C_LIGHT_RED: PackedRgba = PackedRgba::rgb(255, 85, 85);
const C_LIGHT_CYAN: PackedRgba = PackedRgba::rgb(85, 255, 255);
const C_LIGHT_MAGENTA: PackedRgba = PackedRgba::rgb(255, 85, 255);
const C_LIGHT_BLUE: PackedRgba = PackedRgba::rgb(85, 85, 255);
const C_RESET: PackedRgba = PackedRgba::TRANSPARENT;
const SYMBOLS: Symbols = Symbols::UNICODE;
fn indicator_color(indicator: StatusIndicator, colors: &ColorScheme) -> PackedRgba {
match indicator {
StatusIndicator::Success => colors.success,
StatusIndicator::Error => colors.error,
StatusIndicator::Warning => colors.warning,
StatusIndicator::Info => colors.info,
StatusIndicator::Pending | StatusIndicator::Disabled => colors.muted,
StatusIndicator::InProgress => colors.info,
}
}
fn get_colors(high_contrast: bool, color_blind: ColorBlindMode) -> ColorScheme {
if high_contrast {
return ColorScheme {
fg: C_WHITE,
bg: C_BLACK,
highlight: C_YELLOW,
success: C_LIGHT_GREEN,
warning: C_LIGHT_YELLOW,
error: C_LIGHT_RED,
info: C_LIGHT_CYAN,
muted: C_GRAY,
selected_bg: C_WHITE,
selected_fg: C_BLACK,
};
}
match color_blind {
ColorBlindMode::None => ColorScheme {
fg: C_WHITE,
bg: C_RESET,
highlight: C_CYAN,
success: C_GREEN,
warning: C_YELLOW,
error: C_RED,
info: C_BLUE,
muted: C_DARK_GRAY,
selected_bg: C_DARK_GRAY,
selected_fg: C_WHITE,
},
ColorBlindMode::Deuteranopia | ColorBlindMode::Protanopia => ColorScheme {
fg: C_WHITE,
bg: C_RESET,
highlight: C_LIGHT_CYAN,
success: C_LIGHT_CYAN,
warning: C_YELLOW,
error: C_LIGHT_MAGENTA,
info: C_LIGHT_BLUE,
muted: C_DARK_GRAY,
selected_bg: C_DARK_GRAY,
selected_fg: C_WHITE,
},
ColorBlindMode::Tritanopia => ColorScheme {
fg: C_WHITE,
bg: C_RESET,
highlight: C_LIGHT_MAGENTA,
success: C_LIGHT_GREEN,
warning: C_LIGHT_RED,
error: C_RED,
info: C_LIGHT_CYAN,
muted: C_DARK_GRAY,
selected_bg: C_DARK_GRAY,
selected_fg: C_WHITE,
},
}
}
struct ColorScheme {
fg: PackedRgba,
#[allow(dead_code)]
bg: PackedRgba,
highlight: PackedRgba,
success: PackedRgba,
warning: PackedRgba,
error: PackedRgba,
info: PackedRgba,
muted: PackedRgba,
selected_bg: PackedRgba,
selected_fg: PackedRgba,
}
pub fn render(frame: &mut Frame, state: &TuiState) {
let colors = get_colors(state.high_contrast, state.color_blind);
let has_detail = state.show_detail && state.selected_detail().is_some();
let chunks = if has_detail {
Flex::vertical()
.margin(ftui_core::geometry::Sides::all(1))
.constraints([
Constraint::Fixed(3), Constraint::Min(10), Constraint::Fixed(3), Constraint::Fixed(3), ])
.split(frame.bounds())
} else {
Flex::vertical()
.margin(ftui_core::geometry::Sides::all(1))
.constraints([
Constraint::Fixed(3), Constraint::Min(10), Constraint::Fixed(3), ])
.split(frame.bounds())
};
render_header(frame, chunks[0], state, &colors);
render_main_content(frame, chunks[1], state, &colors);
if has_detail {
render_detail_bar(frame, chunks[2], state, &colors);
render_footer(frame, chunks[3], state, &colors);
} else {
render_footer(frame, chunks[2], state, &colors);
}
if state.show_remediation {
render_remediation_overlay(frame, state, &colors);
}
if state.show_help {
render_help_overlay(frame, &colors);
}
if state.filter_mode {
render_filter_input(frame, state, &colors);
}
if let Some(ref error) = state.error {
render_error_bar(frame, error, &colors);
}
if state.last_copied.is_some() {
render_copy_feedback(frame, &colors);
}
if let Some(ref dialog) = state.confirm_dialog {
render_confirm_dialog(frame, dialog, &colors);
}
}
fn render_filter_input(frame: &mut Frame, state: &TuiState, colors: &ColorScheme) {
let area = frame.bounds();
let width = 50.min(area.width.saturating_sub(4));
let x = (area.width.saturating_sub(width)) / 2;
let y = area.height.saturating_sub(8);
let input_area = Rect::new(x, y, width, 3);
frame
.buffer
.fill(input_area, ftui_render::cell::Cell::default());
let cursor = "█";
let input_text = format!("/{}{}", state.filter.query, cursor);
let input = Paragraph::new(Line::from_spans(vec![
Span::styled("Search: ", Style::new().fg(colors.highlight)),
Span::styled(input_text, Style::new().fg(colors.fg)),
]))
.block(
Block::new()
.borders(Borders::ALL)
.title("Filter Build History (Enter to apply, Esc to cancel)")
.border_style(Style::new().fg(colors.highlight)),
);
input.render(input_area, frame);
}
fn render_confirm_dialog(frame: &mut Frame, dialog: &ConfirmDialog, colors: &ColorScheme) {
let area = frame.bounds();
let msg_lines: Vec<&str> = dialog.message.lines().collect();
let content_width = dialog
.title
.len()
.max(msg_lines.iter().map(|l| l.len()).max().unwrap_or(0))
.max(20) as u16
+ 4; let width = content_width.min(area.width.saturating_sub(4));
let height = (msg_lines.len() as u16 + 6).min(area.height.saturating_sub(4));
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let dialog_area = Rect::new(x, y, width, height);
frame
.buffer
.fill(dialog_area, ftui_render::cell::Cell::default());
let mut lines = vec![
Line::from(Span::styled(
&dialog.title,
Style::new().fg(colors.warning).bold(),
)),
Line::from(""),
];
for msg_line in &msg_lines {
lines.push(Line::from(Span::styled(
*msg_line,
Style::new().fg(colors.fg),
)));
}
lines.push(Line::from(""));
lines.push(Line::from_spans(vec![
Span::styled(" [", Style::new().fg(colors.muted)),
Span::styled("Y", Style::new().fg(colors.success).bold()),
Span::styled("]es [", Style::new().fg(colors.muted)),
Span::styled("N", Style::new().fg(colors.error).bold()),
Span::styled("]o", Style::new().fg(colors.muted)),
]));
let confirm_widget = Paragraph::new(ftui_text::Text::from_lines(lines)).block(
Block::new()
.borders(Borders::ALL)
.border_style(Style::new().fg(colors.warning)),
);
confirm_widget.render(dialog_area, frame);
}
fn render_header(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
let (status_sym, status_indicator) = match state.daemon.status {
crate::tui::state::Status::Running => (SYMBOLS.bullet_filled, StatusIndicator::Success),
crate::tui::state::Status::Stopped => (SYMBOLS.bullet_empty, StatusIndicator::Pending),
crate::tui::state::Status::Error => (SYMBOLS.failure, StatusIndicator::Error),
crate::tui::state::Status::Unknown => (SYMBOLS.bullet_empty, StatusIndicator::Pending),
};
let status_label = match state.daemon.status {
crate::tui::state::Status::Running => "Running",
crate::tui::state::Status::Stopped => "Stopped",
crate::tui::state::Status::Error => "Error",
crate::tui::state::Status::Unknown => "Unknown",
};
let status_text = format!("{} {}", status_sym, status_label);
let status_color = match state.daemon.status {
crate::tui::state::Status::Stopped => colors.warning,
_ => indicator_color(status_indicator, colors),
};
let status_style = Style::new().fg(status_color);
let header = Paragraph::new(Line::from_spans(vec![
Span::styled("RCH Dashboard", Style::new().fg(colors.highlight).bold()),
Span::raw(" | "),
Span::styled(status_text, status_style),
Span::raw(" | "),
Span::styled(
format!(
"Workers: {} | Builds: {}",
state.workers.len(),
state.active_builds.len()
),
Style::new().fg(colors.fg),
),
]))
.block(Block::new().borders(Borders::ALL).title("Status"));
header.render(area, frame);
}
fn render_main_content(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
if state.log_view.is_some() {
render_logs_panel(frame, area, state, colors);
return;
}
let chunks = Flex::horizontal()
.constraints([
Constraint::Percentage(40.0), Constraint::Percentage(60.0), ])
.split(area);
render_workers_panel(frame, chunks[0], state, colors);
let build_chunks = Flex::vertical()
.constraints([
Constraint::Percentage(50.0), Constraint::Percentage(50.0), ])
.split(chunks[1]);
render_active_builds_panel(frame, build_chunks[0], state, colors);
render_build_history_panel(frame, build_chunks[1], state, colors);
}
fn render_workers_panel(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
let is_selected = state.selected_panel == Panel::Workers;
let border_style = if is_selected {
Style::new().fg(colors.highlight)
} else {
Style::new()
};
let items: Vec<ListItem> = state
.workers
.iter()
.enumerate()
.map(|(i, w)| {
let (status_icon, status_color) = match w.status {
WorkerStatus::Healthy => (
SYMBOLS.bullet_filled,
indicator_color(StatusIndicator::Success, colors),
),
WorkerStatus::Degraded => (
SYMBOLS.bullet_half,
indicator_color(StatusIndicator::Warning, colors),
),
WorkerStatus::Unreachable => (
SYMBOLS.bullet_empty,
indicator_color(StatusIndicator::Error, colors),
),
WorkerStatus::Draining => (
SYMBOLS.bullet_half,
indicator_color(StatusIndicator::Info, colors),
),
WorkerStatus::Drained => (
SYMBOLS.bullet_empty,
indicator_color(StatusIndicator::Info, colors),
),
WorkerStatus::Disabled => (
SYMBOLS.bullet_empty,
indicator_color(StatusIndicator::Pending, colors),
),
};
let circuit_icon = match w.circuit {
CircuitState::Closed => "",
CircuitState::HalfOpen => " ↻",
CircuitState::Open => " ●",
};
let style = if is_selected && i == state.selected_index {
Style::new().bg(colors.selected_bg).fg(colors.selected_fg)
} else {
Style::new()
};
ListItem::new(Line::from_spans(vec![
Span::styled(status_icon, Style::new().fg(status_color)),
Span::raw(" "),
Span::raw(&w.id),
Span::styled(
format!(" ({}/{})", w.used_slots, w.total_slots),
Style::new().fg(colors.muted),
),
Span::raw(circuit_icon),
]))
.style(style)
})
.collect();
let list = List::new(items).block(
Block::new()
.borders(Borders::ALL)
.title("Workers")
.border_style(border_style),
);
list.render(area, frame);
}
fn render_active_builds_panel(
frame: &mut Frame,
area: Rect,
state: &TuiState,
colors: &ColorScheme,
) {
let is_selected = state.selected_panel == Panel::ActiveBuilds;
let border_style = if is_selected {
Style::new().fg(colors.highlight)
} else {
Style::new()
};
let items: Vec<ListItem> = state
.active_builds
.iter()
.enumerate()
.map(|(i, b)| {
let status_icon = match b.status {
BuildStatus::Pending => SYMBOLS.bullet_empty,
BuildStatus::Syncing => "↑",
BuildStatus::Compiling => SYMBOLS.bullet_half,
BuildStatus::Downloading => "↓",
BuildStatus::Completed => StatusIndicator::Success.symbol(&SYMBOLS),
BuildStatus::Failed => StatusIndicator::Error.symbol(&SYMBOLS),
};
let progress = b
.progress
.as_ref()
.and_then(|p| p.percent)
.map(|p| format!(" {}%", p))
.unwrap_or_default();
let worker = b
.worker
.as_ref()
.map(|w| format!(" → {}", w))
.unwrap_or_default();
let style = if is_selected && i == state.selected_index {
Style::new().bg(colors.selected_bg).fg(colors.selected_fg)
} else {
Style::new()
};
let cmd = truncate_command(&b.command, 40);
ListItem::new(Line::from_spans(vec![
Span::raw(status_icon),
Span::raw(" "),
Span::raw(cmd),
Span::styled(worker, Style::new().fg(colors.info)),
Span::styled(progress, Style::new().fg(colors.muted)),
]))
.style(style)
})
.collect();
let title = if state.active_builds.is_empty() {
"Active Builds (none)"
} else {
"Active Builds"
};
let list = List::new(items).block(
Block::new()
.borders(Borders::ALL)
.title(title)
.border_style(border_style),
);
list.render(area, frame);
}
fn render_build_history_panel(
frame: &mut Frame,
area: Rect,
state: &TuiState,
colors: &ColorScheme,
) {
let is_selected = state.selected_panel == Panel::BuildHistory;
let border_style = if is_selected {
Style::new().fg(colors.highlight)
} else {
Style::new()
};
let filtered = state.filtered_build_history();
let items: Vec<ListItem> = filtered
.iter()
.enumerate()
.map(|(i, b)| {
let (build_indicator, status_icon) = if b.success {
(
StatusIndicator::Success,
StatusIndicator::Success.symbol(&SYMBOLS),
)
} else {
(
StatusIndicator::Error,
StatusIndicator::Error.symbol(&SYMBOLS),
)
};
let status_color = indicator_color(build_indicator, colors);
let style = if is_selected && i == state.selected_index {
Style::new().bg(colors.selected_bg).fg(colors.selected_fg)
} else {
Style::new()
};
let duration = format_duration_ms(b.duration_ms);
let worker = b.worker.as_deref().unwrap_or("local");
let cmd = truncate_command(&b.command, 30);
ListItem::new(Line::from_spans(vec![
Span::styled(status_icon, Style::new().fg(status_color)),
Span::raw(" "),
Span::raw(cmd),
Span::raw(" @ "),
Span::styled(worker, Style::new().fg(colors.highlight)),
Span::styled(format!(" ({})", duration), Style::new().fg(colors.muted)),
]))
.style(style)
})
.collect();
let sort_indicator = state.build_history_sort.display_name();
let title = if state.filter_mode || !state.filter.query.is_empty() {
format!(
"Build History [{}/{}] (sorted: {}) (/ to filter)",
filtered.len(),
state.build_history.len(),
sort_indicator
)
} else {
format!(
"Build History [{}] (sorted: {})",
state.build_history.len(),
sort_indicator
)
};
let list = List::new(items).block(
Block::new()
.borders(Borders::ALL)
.title(title.as_str())
.border_style(border_style),
);
list.render(area, frame);
}
fn format_duration_ms(ms: u64) -> String {
if ms < 1000 {
format!("{}ms", ms)
} else if ms < 60_000 {
format!("{:.1}s", ms as f64 / 1000.0)
} else {
format!("{:.1}m", ms as f64 / 60_000.0)
}
}
fn truncate_to_char_boundary(value: &str, max_len: usize) -> &str {
if value.len() <= max_len {
return value;
}
let mut end = max_len;
while end > 0 && !value.is_char_boundary(end) {
end -= 1;
}
&value[..end]
}
fn truncate_command(cmd: &str, max_len: usize) -> String {
if cmd.len() <= max_len {
return cmd.to_string();
}
let important_suffixes = ["--release", "--test", "--bench", "--features"];
for suffix in important_suffixes {
if cmd.ends_with(suffix) {
let available = max_len.saturating_sub(suffix.len() + 4); if available > 8 {
return format!(
"{}... {}",
truncate_to_char_boundary(cmd, available),
suffix
);
}
}
}
if let Some(p_idx) = cmd.rfind(" -p ") {
let suffix_start = p_idx;
let suffix = &cmd[suffix_start..];
if suffix.len() < max_len / 2 {
let available = max_len.saturating_sub(suffix.len() + 3);
if available > 8 {
return format!("{}...{}", truncate_to_char_boundary(cmd, available), suffix);
}
}
}
format!(
"{}...",
truncate_to_char_boundary(cmd, max_len.saturating_sub(3))
)
}
fn render_detail_bar(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
let detail = state.selected_detail().unwrap_or_default();
let paragraph = Paragraph::new(Line::from(Span::styled(detail, Style::new().fg(colors.fg))))
.block(
Block::new()
.borders(Borders::ALL)
.title("Selected")
.border_style(Style::new().fg(colors.muted)),
);
paragraph.render(area, frame);
}
fn render_footer(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
let hints = if state.filter_mode {
vec![("Esc", "Exit filter"), ("Enter", "Apply")]
} else if state.log_view.is_some() {
vec![("Esc", "Close logs"), ("↑/↓", "Scroll"), ("y", "Copy")]
} else {
vec![
("q", "Quit"),
("↑/↓", "Navigate"),
("Tab", "Panel"),
("r", "Refresh"),
("R", "Remediation"),
("?", "Help"),
("/", "Filter"),
]
};
let spans: Vec<Span> = hints
.iter()
.flat_map(|(key, desc)| {
vec![
Span::styled(*key, Style::new().fg(colors.highlight).bold()),
Span::raw(": "),
Span::styled(*desc, Style::new().fg(colors.fg)),
Span::raw(" | "),
]
})
.collect();
let footer = Paragraph::new(Line::from_spans(spans))
.block(Block::new().borders(Borders::ALL).title("Controls"));
footer.render(area, frame);
}
fn render_help_overlay(frame: &mut Frame, colors: &ColorScheme) {
let area = frame.bounds();
let width = 60.min(area.width.saturating_sub(4));
let height = 50.min(area.height.saturating_sub(4));
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let help_area = Rect::new(x, y, width, height);
frame
.buffer
.fill(help_area, ftui_render::cell::Cell::default());
let help_text = vec![
Line::from(Span::styled(
"RCH Dashboard Help",
Style::new().fg(colors.highlight).bold(),
)),
Line::from(""),
Line::from_spans(vec![Span::styled("Navigation", Style::new().bold())]),
Line::from(" ↑/k, ↓/j Move selection up/down"),
Line::from(" →/l, Tab Next panel"),
Line::from(" ←/h, S-Tab Previous panel"),
Line::from(" 1-4 Jump to panel by number"),
Line::from(" H / L Jump to first / last panel"),
Line::from(" g / G Jump to first / last item"),
Line::from(" Enter Select/expand item"),
Line::from(" Backspace Go back / Close log view"),
Line::from(""),
Line::from_spans(vec![Span::styled(
"Scrolling (Log View)",
Style::new().bold(),
)]),
Line::from(" PgUp/PgDn Scroll page up/down"),
Line::from(" g Jump to top"),
Line::from(" G Jump to bottom (resume auto-scroll)"),
Line::from(""),
Line::from_spans(vec![Span::styled("Actions", Style::new().bold())]),
Line::from(" r / R Refresh data / toggle remediation overlay"),
Line::from(" y Copy selected item"),
Line::from(""),
Line::from_spans(vec![Span::styled(
"Worker Actions (Workers panel)",
Style::new().bold(),
)]),
Line::from(" d Drain selected worker"),
Line::from(" e Enable selected worker"),
Line::from(" D Drain ALL workers"),
Line::from(" E Enable ALL workers"),
Line::from(""),
Line::from_spans(vec![Span::styled(
"Build Actions (Active Builds panel)",
Style::new().bold(),
)]),
Line::from(" x Cancel selected build (SIGTERM)"),
Line::from(" X Force kill selected build (SIGKILL)"),
Line::from(""),
Line::from_spans(vec![Span::styled("Search & Filter", Style::new().bold())]),
Line::from(" / Open filter (Build History panel)"),
Line::from(" Enter Apply filter"),
Line::from(" Esc Cancel filter"),
Line::from(""),
Line::from_spans(vec![Span::styled(
"Sort (Build History panel)",
Style::new().bold(),
)]),
Line::from(" s Cycle sort order"),
Line::from(" S Reverse sort direction"),
Line::from(""),
Line::from_spans(vec![Span::styled("General", Style::new().bold())]),
Line::from(" q, Esc Quit / Close overlay"),
Line::from(" ?, F1 Toggle this help"),
Line::from(" Ctrl+C Force quit"),
Line::from(""),
Line::from(Span::styled(
"Press any key to close",
Style::new().fg(colors.muted),
)),
];
let help = Paragraph::new(ftui_text::Text::from_lines(help_text))
.block(
Block::new()
.borders(Borders::ALL)
.title("Help")
.border_style(Style::new().fg(colors.highlight)),
)
.alignment(Alignment::Left)
.wrap(WrapMode::Word);
help.render(help_area, frame);
}
fn render_remediation_overlay(frame: &mut Frame, state: &TuiState, colors: &ColorScheme) {
use rch_common::remediation_view::{BandSeverity, RemediationActionClass};
let area = frame.bounds();
let width = 76.min(area.width.saturating_sub(4));
let height = 44.min(area.height.saturating_sub(4));
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let overlay_area = Rect::new(x, y, width, height);
frame
.buffer
.fill(overlay_area, ftui_render::cell::Cell::default());
let sev_color = |sev: BandSeverity| match sev {
BandSeverity::Ok => colors.success,
BandSeverity::Info => colors.info,
BandSeverity::Warn => colors.warning,
BandSeverity::Critical => colors.error,
};
let class_tag = |class: RemediationActionClass| match class {
RemediationActionClass::Healthy => ("OK", colors.success),
RemediationActionClass::NormalFailOpen => ("FAIL-OPEN", colors.muted),
RemediationActionClass::SelfHealingInProgress => ("HEALING", colors.info),
RemediationActionClass::OperatorActionRequired => ("ACTION", colors.error),
};
let mut lines: Vec<Line> = Vec::new();
match &state.remediation {
None => {
lines.push(Line::from(Span::styled(
"Remediation view unavailable",
Style::new().fg(colors.warning).bold(),
)));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"The daemon did not report a remediation snapshot. It may be",
Style::new().fg(colors.muted),
)));
lines.push(Line::from(Span::styled(
"starting up, or predates this feature. Press 'r' to refresh.",
Style::new().fg(colors.muted),
)));
}
Some(view) => {
let (tag, tag_color) = class_tag(view.overall);
lines.push(Line::from_spans(vec![
Span::styled("Overall posture: ", Style::new().bold()),
Span::styled(
view.overall.label().to_string(),
Style::new().fg(tag_color).bold(),
),
Span::styled(format!(" [{tag}]"), Style::new().fg(tag_color)),
]));
lines.push(Line::from(Span::styled(
"operator-action vs self-healing vs normal fail-open",
Style::new().fg(colors.muted),
)));
lines.push(Line::from(""));
for band in &view.bands {
let (btag, btag_color) = class_tag(band.action_class);
lines.push(Line::from_spans(vec![
Span::styled("● ", Style::new().fg(sev_color(band.severity))),
Span::styled(format!("{:<20}", band.title), Style::new().bold()),
Span::styled(format!(" {}", band.headline), Style::new().fg(colors.fg)),
]));
lines.push(Line::from_spans(vec![
Span::styled(" ", Style::new()),
Span::styled(format!("[{btag}]"), Style::new().fg(btag_color)),
Span::styled(
band.reason_code
.as_ref()
.map_or(String::new(), |r| format!(" reason: {r}")),
Style::new().fg(colors.muted),
),
]));
for detail in &band.detail_lines {
lines.push(Line::from(Span::styled(
format!(" {detail}"),
Style::new().fg(colors.muted),
)));
}
}
lines.push(Line::from(""));
if view.incidents.is_empty() {
lines.push(Line::from(Span::styled(
"Recent incidents: none",
Style::new().fg(colors.success),
)));
} else {
lines.push(Line::from_spans(vec![Span::styled(
"Recent incidents",
Style::new().bold(),
)]));
for inc in view.incidents.iter().take(5) {
let who = inc
.worker_id
.as_deref()
.map_or(String::new(), |w| format!(" [{w}]"));
lines.push(Line::from(Span::styled(
format!(
" {} {}{} — {} ({}s ago)",
inc.reason_code, inc.event_type, who, inc.summary, inc.age_secs
),
Style::new().fg(colors.muted),
)));
}
}
}
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Press 'R' or any key to close",
Style::new().fg(colors.muted),
)));
let overlay = Paragraph::new(ftui_text::Text::from_lines(lines))
.block(
Block::new()
.borders(Borders::ALL)
.title("Remediation")
.border_style(Style::new().fg(colors.highlight)),
)
.alignment(Alignment::Left)
.wrap(WrapMode::Word);
overlay.render(overlay_area, frame);
}
fn render_error_bar(frame: &mut Frame, error: &str, colors: &ColorScheme) {
let area = frame.bounds();
let error_area = Rect::new(
1,
area.height.saturating_sub(2),
area.width.saturating_sub(2),
1,
);
let error_msg = Paragraph::new(Line::from_spans(vec![
Span::styled("Error: ", Style::new().fg(colors.error).bold()),
Span::styled(error, Style::new().fg(colors.error)),
]));
error_msg.render(error_area, frame);
}
fn render_copy_feedback(frame: &mut Frame, colors: &ColorScheme) {
let area = frame.bounds();
let message = "Copied to clipboard!";
let max_width = area.width.saturating_sub(2).max(1);
let width = (message.len() as u16).min(max_width);
let x = area.width.saturating_sub(width.saturating_add(1));
let feedback_area = Rect::new(x, 1, width, 1);
let visible = &message[..width as usize];
let feedback = Paragraph::new(Line::from(Span::styled(
visible,
Style::new().fg(colors.success),
)));
feedback.render(feedback_area, frame);
}
fn render_logs_panel(frame: &mut Frame, area: Rect, state: &TuiState, colors: &ColorScheme) {
let log_view = match &state.log_view {
Some(lv) => lv,
None => return,
};
let is_selected = state.selected_panel == Panel::Logs;
let border_style = if is_selected {
Style::new().fg(colors.highlight)
} else {
Style::new()
};
let visible_height = area.height.saturating_sub(2) as usize; let total_lines = log_view.lines.len();
let scroll_offset = log_view
.scroll_offset
.min(total_lines.saturating_sub(visible_height));
let end_line = (scroll_offset + visible_height).min(total_lines);
let items: Vec<ListItem> = log_view
.lines
.iter()
.skip(scroll_offset)
.take(visible_height)
.map(|line| {
let style = if line.contains("error") || line.contains("Error") {
Style::new().fg(colors.error)
} else if line.contains("warning") || line.contains("Warning") {
Style::new().fg(colors.warning)
} else if line.contains("Compiling") || line.contains("Building") {
Style::new().fg(colors.info)
} else if line.contains("Finished") {
Style::new().fg(colors.success)
} else {
Style::new().fg(colors.fg)
};
ListItem::new(Line::from(Span::styled(line.as_str(), style)))
})
.collect();
let scroll_indicator = if total_lines > visible_height {
format!(" [{}-{}/{}]", scroll_offset + 1, end_line, total_lines)
} else {
String::new()
};
let auto_scroll_indicator = if log_view.auto_scroll { " [AUTO]" } else { "" };
let title = format!(
"Build Logs: {}{}{} (Esc to close, PgUp/PgDn to scroll)",
log_view.build_id, scroll_indicator, auto_scroll_indicator,
);
let list = List::new(items).block(
Block::new()
.borders(Borders::ALL)
.title(title.as_str())
.border_style(border_style),
);
list.render(area, frame);
if total_lines > visible_height {
render_scrollbar(
frame,
area,
scroll_offset,
total_lines,
visible_height,
colors,
);
}
}
fn render_scrollbar(
frame: &mut Frame,
area: Rect,
offset: usize,
total: usize,
visible: usize,
colors: &ColorScheme,
) {
let scrollbar_height = area.height.saturating_sub(2) as usize; if scrollbar_height == 0 || total == 0 {
return;
}
let thumb_size = ((visible as f64 / total as f64) * scrollbar_height as f64).max(1.0) as u16;
let scroll_range = total.saturating_sub(visible);
let thumb_pos = if scroll_range > 0 {
((offset as f64 / scroll_range as f64) * (scrollbar_height as f64 - thumb_size as f64))
as u16
} else {
0
};
let x = area.x + area.width - 1;
for i in 0..scrollbar_height as u16 {
let y = area.y + 1 + i; let char = if i >= thumb_pos && i < thumb_pos + thumb_size {
"█" } else {
"│" };
let style = if i >= thumb_pos && i < thumb_pos + thumb_size {
Style::new().fg(colors.highlight)
} else {
Style::new().fg(colors.muted)
};
Paragraph::new(Line::from(Span::styled(char, style))).render(Rect::new(x, y, 1, 1), frame);
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::tui::state::{
ActiveBuild, BuildProgress, BuildStatus, CircuitState, FilterState, LogViewState, Panel,
TuiState, WorkerState, WorkerStatus,
};
use chrono::Utc;
use ftui_render::buffer::Buffer;
use ftui_render::grapheme_pool::GraphemePool;
use tracing::info;
fn init_test_logging() {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_max_level(tracing::Level::DEBUG)
.try_init();
}
fn buffer_to_string(buffer: &Buffer, pool: &GraphemePool) -> String {
let mut out = String::new();
let width = buffer.width();
let height = buffer.height();
for y in 0..height {
for x in 0..width {
if let Some(cell) = buffer.get(x, y) {
if cell.content.is_continuation() {
continue;
}
if cell.content.is_empty() {
out.push(' ');
} else if let Some(c) = cell.content.as_char() {
out.push(c);
} else if let Some(gid) = cell.content.grapheme_id() {
if let Some(s) = pool.get(gid) {
out.push_str(s);
} else {
out.push(' ');
}
} else {
out.push(' ');
}
} else {
out.push(' ');
}
}
out.push('\n');
}
out
}
fn render_to_string<F>(width: u16, height: u16, mut draw: F) -> String
where
F: FnMut(&mut Frame),
{
let mut pool = GraphemePool::new();
let mut frame = Frame::new(width, height, &mut pool);
draw(&mut frame);
buffer_to_string(&frame.buffer, &pool)
}
fn sample_worker(id: &str, status: WorkerStatus, circuit: CircuitState) -> WorkerState {
WorkerState {
id: id.to_string(),
host: "worker.local".to_string(),
status,
circuit,
total_slots: 8,
used_slots: 2,
latency_ms: 10,
last_seen: Utc::now(),
builds_completed: 3,
}
}
fn sample_active_build(id: &str, command: &str) -> ActiveBuild {
ActiveBuild {
id: id.to_string(),
command: command.to_string(),
worker: Some("worker-1".to_string()),
started_at: Utc::now(),
progress: Some(BuildProgress {
phase: "compiling".to_string(),
percent: Some(42),
current_file: None,
}),
status: BuildStatus::Compiling,
}
}
#[test]
fn test_render_workers_panel_contains_ids() {
init_test_logging();
info!("TEST START: test_render_workers_panel_contains_ids");
let state = TuiState {
selected_panel: Panel::Workers,
workers: vec![
sample_worker("worker-a", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("worker-b", WorkerStatus::Degraded, CircuitState::HalfOpen),
],
..Default::default()
};
let content = render_to_string(60, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_workers_panel(f, Rect::new(0, 0, 60, 10), &state, &colors);
});
info!("VERIFY: content contains worker ids");
assert!(content.contains("worker-a"));
assert!(content.contains("worker-b"));
info!("TEST PASS: test_render_workers_panel_contains_ids");
}
#[test]
fn test_color_blind_palette_selection() {
init_test_logging();
info!("TEST START: test_color_blind_palette_selection");
let deuter = get_colors(false, ColorBlindMode::Deuteranopia);
let tritan = get_colors(false, ColorBlindMode::Tritanopia);
info!(
"VERIFY: deuter_highlight={:?} tritan_highlight={:?}",
deuter.highlight, tritan.highlight
);
assert_eq!(deuter.highlight, C_LIGHT_CYAN);
assert_eq!(tritan.highlight, C_LIGHT_MAGENTA);
info!("TEST PASS: test_color_blind_palette_selection");
}
#[test]
fn test_render_active_builds_panel_shows_command() {
init_test_logging();
info!("TEST START: test_render_active_builds_panel_shows_command");
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: vec![sample_active_build("b1", "cargo build")],
..Default::default()
};
let content = render_to_string(80, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 80, 10), &state, &colors);
});
assert!(content.contains("cargo build"));
assert!(content.contains("worker-1"));
info!("TEST PASS: test_render_active_builds_panel_shows_command");
}
#[test]
fn test_render_build_history_panel_shows_filtered_title() {
init_test_logging();
info!("TEST START: test_render_build_history_panel_shows_filtered_title");
let mut state = TuiState {
selected_panel: Panel::BuildHistory,
filter: FilterState {
query: "build".to_string(),
..Default::default()
},
..Default::default()
};
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h1".to_string(),
command: "cargo build".to_string(),
worker: Some("worker-1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1200,
success: true,
exit_code: Some(0),
});
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h2".to_string(),
command: "cargo test".to_string(),
worker: Some("worker-2".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1500,
success: false,
exit_code: Some(1),
});
let content = render_to_string(80, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_build_history_panel(f, Rect::new(0, 0, 80, 10), &state, &colors);
});
assert!(content.contains("Build History [1/2]"));
info!("TEST PASS: test_render_build_history_panel_shows_filtered_title");
}
#[test]
fn test_render_logs_panel_scroll_indicator() {
init_test_logging();
info!("TEST START: test_render_logs_panel_scroll_indicator");
let mut log_view = LogViewState::default();
for i in 0..10 {
log_view.lines.push_back(format!("line {}", i));
}
log_view.scroll_offset = 2;
let state = TuiState {
log_view: Some(log_view),
selected_panel: Panel::Logs,
..Default::default()
};
let content = render_to_string(80, 6, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_logs_panel(f, Rect::new(0, 0, 80, 6), &state, &colors);
});
assert!(content.contains("[3-6/10]"));
assert!(content.contains("AUTO"));
info!("TEST PASS: test_render_logs_panel_scroll_indicator");
}
#[test]
fn test_render_help_overlay() {
init_test_logging();
info!("TEST START: test_render_help_overlay");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(content.contains("RCH Dashboard Help"));
info!("TEST PASS: test_render_help_overlay");
}
#[test]
fn test_help_overlay_contains_filter_shortcut() {
init_test_logging();
info!("TEST START: test_help_overlay_contains_filter_shortcut");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
assert!(
content.contains("/"),
"Filter shortcut (/) must be documented"
);
assert!(
content.contains("filter") || content.contains("Filter") || content.contains("Search"),
"Filter functionality must be described"
);
info!("TEST PASS: test_help_overlay_contains_filter_shortcut");
}
#[test]
fn test_help_overlay_contains_all_navigation_shortcuts() {
init_test_logging();
info!("TEST START: test_help_overlay_contains_all_navigation_shortcuts");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 32, |f| render(f, &state));
info!("VERIFY: checking navigation shortcuts");
assert!(
content.contains("j") || content.contains("↓"),
"Down navigation must be documented"
);
assert!(
content.contains("k") || content.contains("↑"),
"Up navigation must be documented"
);
assert!(
content.contains("Tab"),
"Tab for panel switching must be documented"
);
assert!(
content.contains("Enter"),
"Enter for selection must be documented"
);
info!("TEST PASS: test_help_overlay_contains_all_navigation_shortcuts");
}
#[test]
fn test_help_overlay_contains_all_action_shortcuts() {
init_test_logging();
info!("TEST START: test_help_overlay_contains_all_action_shortcuts");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 32, |f| render(f, &state));
info!("VERIFY: checking action shortcuts");
assert!(content.contains("r"), "Refresh shortcut must be documented");
assert!(content.contains("y"), "Copy shortcut must be documented");
assert!(content.contains("d"), "Drain shortcut must be documented");
assert!(content.contains("e"), "Enable shortcut must be documented");
info!("TEST PASS: test_help_overlay_contains_all_action_shortcuts");
}
#[test]
fn test_help_overlay_contains_scrolling_shortcuts() {
init_test_logging();
info!("TEST START: test_help_overlay_contains_scrolling_shortcuts");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 32, |f| render(f, &state));
info!("VERIFY: checking scrolling shortcuts");
assert!(
content.contains("PgUp") || content.contains("PageUp") || content.contains("Page"),
"Page up must be documented"
);
assert!(content.contains("g"), "Jump to top (g) must be documented");
assert!(
content.contains("G"),
"Jump to bottom (G) must be documented"
);
info!("TEST PASS: test_help_overlay_contains_scrolling_shortcuts");
}
#[test]
fn test_help_overlay_contains_general_shortcuts() {
init_test_logging();
info!("TEST START: test_help_overlay_contains_general_shortcuts");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
info!("VERIFY: checking general shortcuts");
assert!(content.contains("q"), "Quit shortcut must be documented");
assert!(
content.contains("Esc"),
"Escape shortcut must be documented"
);
assert!(
content.contains("Toggle") || content.contains("Help") || content.contains("help"),
"Help toggle shortcut must be documented"
);
assert!(
content.contains("F1"),
"F1 help shortcut must be documented"
);
assert!(content.contains("Ctrl"), "Ctrl+C must be documented");
info!("TEST PASS: test_help_overlay_contains_general_shortcuts");
}
#[test]
fn test_help_overlay_section_ordering() {
init_test_logging();
info!("TEST START: test_help_overlay_section_ordering");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
info!("VERIFY: checking section order");
let nav_pos = content.find("Navigation");
let actions_pos = content.find("Actions");
let general_pos = content.find("General");
assert!(nav_pos.is_some(), "Navigation section must exist");
assert!(actions_pos.is_some(), "Actions section must exist");
assert!(general_pos.is_some(), "General section must exist");
let nav = nav_pos.unwrap();
let actions = actions_pos.unwrap();
let general = general_pos.unwrap();
assert!(nav < actions, "Navigation should come before Actions");
assert!(actions < general, "Actions should come before General");
info!("TEST PASS: test_help_overlay_section_ordering");
}
#[test]
fn test_help_overlay_fits_80x24() {
init_test_logging();
info!("TEST START: test_help_overlay_fits_80x24");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(content.contains("Help"), "Help title should be visible");
assert!(
content.contains("Navigation") || content.contains("RCH Dashboard"),
"At least the title or first section should be visible"
);
info!("TEST PASS: test_help_overlay_fits_80x24");
}
#[test]
fn test_help_overlay_fits_minimal_terminal() {
init_test_logging();
info!("TEST START: test_help_overlay_fits_minimal_terminal");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(40, 12, |f| render(f, &state));
assert!(!content.is_empty(), "Content should not be empty");
info!("TEST PASS: test_help_overlay_fits_minimal_terminal");
}
#[test]
fn test_help_overlay_worker_actions_context() {
init_test_logging();
info!("TEST START: test_help_overlay_worker_actions_context");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 32, |f| render(f, &state));
info!("VERIFY: worker actions should mention Workers panel");
assert!(
content.contains("Workers") || content.contains("worker"),
"Worker actions should mention Workers panel context"
);
info!("TEST PASS: test_help_overlay_worker_actions_context");
}
#[test]
fn test_help_overlay_filter_context() {
init_test_logging();
info!("TEST START: test_help_overlay_filter_context");
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
info!("VERIFY: filter should mention Build History panel");
assert!(
content.contains("Build") || content.contains("History"),
"Filter should mention Build History panel context"
);
info!("TEST PASS: test_help_overlay_filter_context");
}
#[test]
fn test_render_filter_input_overlay() {
init_test_logging();
info!("TEST START: test_render_filter_input_overlay");
let state = TuiState {
filter_mode: true,
filter: FilterState {
query: "abc".to_string(),
..Default::default()
},
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(content.contains("Search:"));
assert!(content.contains("/abc"));
info!("TEST PASS: test_render_filter_input_overlay");
}
#[test]
fn test_render_error_bar() {
init_test_logging();
info!("TEST START: test_render_error_bar");
let state = TuiState {
error: Some("daemon down".to_string()),
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(content.contains("Error:"));
assert!(content.contains("daemon down"));
info!("TEST PASS: test_render_error_bar");
}
#[test]
fn test_render_copy_feedback() {
init_test_logging();
info!("TEST START: test_render_copy_feedback");
let state = TuiState {
last_copied: Some("payload".to_string()),
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(content.contains("Copied to clipboard!"));
info!("TEST PASS: test_render_copy_feedback");
}
#[test]
fn test_render_minimum_size_no_panic() {
init_test_logging();
info!("TEST START: test_render_minimum_size_no_panic");
let state = TuiState::default();
let result = std::panic::catch_unwind(|| {
let _ = render_to_string(1, 1, |f| render(f, &state));
});
assert!(result.is_ok());
info!("TEST PASS: test_render_minimum_size_no_panic");
}
#[test]
fn test_format_duration_ms_milliseconds() {
init_test_logging();
info!("TEST START: test_format_duration_ms_milliseconds");
assert_eq!(format_duration_ms(0), "0ms");
assert_eq!(format_duration_ms(1), "1ms");
assert_eq!(format_duration_ms(500), "500ms");
assert_eq!(format_duration_ms(999), "999ms");
info!("TEST PASS: test_format_duration_ms_milliseconds");
}
#[test]
fn test_format_duration_ms_seconds() {
init_test_logging();
info!("TEST START: test_format_duration_ms_seconds");
assert_eq!(format_duration_ms(1000), "1.0s");
assert_eq!(format_duration_ms(1500), "1.5s");
assert_eq!(format_duration_ms(30000), "30.0s");
assert_eq!(format_duration_ms(59999), "60.0s");
info!("TEST PASS: test_format_duration_ms_seconds");
}
#[test]
fn test_format_duration_ms_minutes() {
init_test_logging();
info!("TEST START: test_format_duration_ms_minutes");
assert_eq!(format_duration_ms(60000), "1.0m");
assert_eq!(format_duration_ms(90000), "1.5m");
assert_eq!(format_duration_ms(300000), "5.0m");
info!("TEST PASS: test_format_duration_ms_minutes");
}
#[test]
fn test_high_contrast_mode_colors() {
init_test_logging();
info!("TEST START: test_high_contrast_mode_colors");
let high_contrast = get_colors(true, ColorBlindMode::None);
assert_eq!(high_contrast.fg, C_WHITE);
assert_eq!(high_contrast.bg, C_BLACK);
assert_eq!(high_contrast.highlight, C_YELLOW);
assert_eq!(high_contrast.success, C_LIGHT_GREEN);
assert_eq!(high_contrast.error, C_LIGHT_RED);
assert_eq!(high_contrast.selected_bg, C_WHITE);
assert_eq!(high_contrast.selected_fg, C_BLACK);
info!("TEST PASS: test_high_contrast_mode_colors");
}
#[test]
fn test_color_blind_protanopia_same_as_deuteranopia() {
init_test_logging();
info!("TEST START: test_color_blind_protanopia_same_as_deuteranopia");
let proto = get_colors(false, ColorBlindMode::Protanopia);
let deuter = get_colors(false, ColorBlindMode::Deuteranopia);
assert_eq!(proto.highlight, deuter.highlight);
assert_eq!(proto.success, deuter.success);
assert_eq!(proto.error, deuter.error);
assert_eq!(proto.highlight, C_LIGHT_CYAN);
assert_eq!(proto.error, C_LIGHT_MAGENTA);
info!("TEST PASS: test_color_blind_protanopia_same_as_deuteranopia");
}
#[test]
fn test_color_blind_tritanopia_distinct_palette() {
init_test_logging();
info!("TEST START: test_color_blind_tritanopia_distinct_palette");
let tritan = get_colors(false, ColorBlindMode::Tritanopia);
let normal = get_colors(false, ColorBlindMode::None);
assert_eq!(tritan.highlight, C_LIGHT_MAGENTA);
assert_ne!(tritan.highlight, normal.highlight);
assert_eq!(tritan.warning, C_LIGHT_RED);
info!("TEST PASS: test_color_blind_tritanopia_distinct_palette");
}
#[test]
fn test_normal_mode_colors() {
init_test_logging();
info!("TEST START: test_normal_mode_colors");
let normal = get_colors(false, ColorBlindMode::None);
assert_eq!(normal.fg, C_WHITE);
assert_eq!(normal.bg, C_RESET);
assert_eq!(normal.highlight, C_CYAN);
assert_eq!(normal.success, C_GREEN);
assert_eq!(normal.warning, C_YELLOW);
assert_eq!(normal.error, C_RED);
assert_eq!(normal.info, C_BLUE);
info!("TEST PASS: test_normal_mode_colors");
}
#[test]
fn test_render_worker_all_status_icons() {
init_test_logging();
info!("TEST START: test_render_worker_all_status_icons");
let state = TuiState {
selected_panel: Panel::Workers,
workers: vec![
sample_worker("healthy-w", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("degraded-w", WorkerStatus::Degraded, CircuitState::Closed),
sample_worker(
"unreachable-w",
WorkerStatus::Unreachable,
CircuitState::Closed,
),
sample_worker("draining-w", WorkerStatus::Draining, CircuitState::Closed),
],
..Default::default()
};
let content = render_to_string(60, 12, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_workers_panel(f, Rect::new(0, 0, 60, 12), &state, &colors);
});
assert!(content.contains("healthy-w"));
assert!(content.contains("degraded-w"));
assert!(content.contains("unreachable-w"));
assert!(content.contains("draining-w"));
info!("TEST PASS: test_render_worker_all_status_icons");
}
#[test]
fn test_render_worker_circuit_state_icons() {
init_test_logging();
info!("TEST START: test_render_worker_circuit_state_icons");
let state = TuiState {
selected_panel: Panel::Workers,
workers: vec![
sample_worker("closed-w", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("half-w", WorkerStatus::Healthy, CircuitState::HalfOpen),
sample_worker("open-w", WorkerStatus::Healthy, CircuitState::Open),
],
..Default::default()
};
let content = render_to_string(60, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_workers_panel(f, Rect::new(0, 0, 60, 10), &state, &colors);
});
assert!(content.contains("half-w"));
assert!(content.contains("open-w"));
assert!(
content.contains("↻") || content.contains("●"),
"Circuit icons should use unicode symbols for terminal compatibility"
);
info!("TEST PASS: test_render_worker_circuit_state_icons");
}
#[test]
fn test_circuit_icons_no_emoji() {
init_test_logging();
info!("TEST START: test_circuit_icons_no_emoji");
let state = TuiState {
selected_panel: Panel::Workers,
workers: vec![
sample_worker("w1", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("w2", WorkerStatus::Healthy, CircuitState::HalfOpen),
sample_worker("w3", WorkerStatus::Healthy, CircuitState::Open),
],
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(
!content.contains("🔴"),
"Should not contain red circle emoji"
);
assert!(
!content.contains("⚡"),
"Should not contain lightning emoji"
);
assert!(
!content.contains("🟢"),
"Should not contain green circle emoji"
);
assert!(
!content.contains("🟡"),
"Should not contain yellow circle emoji"
);
info!("TEST PASS: test_circuit_icons_no_emoji");
}
#[test]
fn test_status_icons_terminal_compatible() {
init_test_logging();
info!("TEST START: test_status_icons_terminal_compatible");
let symbols = ["✓", "✗", "●", "↻", "◐", "○"];
for sym in symbols {
assert!(
sym.chars().all(|c| (c as u32) < 0x10000),
"Symbol {} should be in BMP for terminal compatibility",
sym
);
}
info!("TEST PASS: test_status_icons_terminal_compatible");
}
#[test]
fn test_render_workers_slot_display() {
init_test_logging();
info!("TEST START: test_render_workers_slot_display");
let mut worker = sample_worker("slot-w", WorkerStatus::Healthy, CircuitState::Closed);
worker.used_slots = 5;
worker.total_slots = 10;
let state = TuiState {
selected_panel: Panel::Workers,
workers: vec![worker],
..Default::default()
};
let content = render_to_string(60, 6, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_workers_panel(f, Rect::new(0, 0, 60, 6), &state, &colors);
});
assert!(content.contains("(5/10)"));
info!("TEST PASS: test_render_workers_slot_display");
}
#[test]
fn test_render_all_build_statuses() {
init_test_logging();
info!("TEST START: test_render_all_build_statuses");
let builds = vec![
ActiveBuild {
id: "b1".to_string(),
command: "pending cmd".to_string(),
worker: None,
started_at: Utc::now(),
progress: None,
status: BuildStatus::Pending,
},
ActiveBuild {
id: "b2".to_string(),
command: "syncing cmd".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: None,
status: BuildStatus::Syncing,
},
ActiveBuild {
id: "b3".to_string(),
command: "compiling cmd".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: Some(BuildProgress {
phase: "compiling".to_string(),
percent: Some(50),
current_file: None,
}),
status: BuildStatus::Compiling,
},
ActiveBuild {
id: "b4".to_string(),
command: "downloading cmd".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: None,
status: BuildStatus::Downloading,
},
];
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: builds,
..Default::default()
};
let content = render_to_string(80, 12, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 80, 12), &state, &colors);
});
assert!(content.contains("pending cmd"));
assert!(content.contains("syncing cmd"));
assert!(content.contains("compiling cmd"));
assert!(content.contains("downloading cmd"));
info!("TEST PASS: test_render_all_build_statuses");
}
#[test]
fn test_render_empty_active_builds_shows_none() {
init_test_logging();
info!("TEST START: test_render_empty_active_builds_shows_none");
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: vec![],
..Default::default()
};
let content = render_to_string(60, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 60, 8), &state, &colors);
});
assert!(content.contains("Active Builds (none)"));
info!("TEST PASS: test_render_empty_active_builds_shows_none");
}
#[test]
fn test_active_build_command_truncation() {
init_test_logging();
info!("TEST START: test_active_build_command_truncation");
let long_command = "cargo build --release --features=abc,def,ghi,jkl,mno,pqr";
let build = ActiveBuild {
id: "b1".to_string(),
command: long_command.to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: None,
status: BuildStatus::Compiling,
};
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: vec![build],
..Default::default()
};
let content = render_to_string(80, 6, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 80, 6), &state, &colors);
});
assert!(content.contains("..."));
assert!(!content.contains(long_command));
info!("TEST PASS: test_active_build_command_truncation");
}
#[test]
fn test_history_command_truncation() {
init_test_logging();
info!("TEST START: test_history_command_truncation");
let long_command = "cargo build --release --features=abc,def,ghi,jkl";
let mut state = TuiState {
selected_panel: Panel::BuildHistory,
..Default::default()
};
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h1".to_string(),
command: long_command.to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1200,
success: true,
exit_code: Some(0),
});
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_build_history_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(content.contains("..."));
info!("TEST PASS: test_history_command_truncation");
}
#[test]
fn test_render_header_all_daemon_statuses() {
init_test_logging();
info!("TEST START: test_render_header_all_daemon_statuses");
use crate::tui::state::Status;
for (status, expected_text) in [
(Status::Running, "Running"),
(Status::Stopped, "Stopped"),
(Status::Error, "Error"),
(Status::Unknown, "Unknown"),
] {
let mut state = TuiState::default();
state.daemon.status = status;
let content = render_to_string(80, 5, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_header(f, Rect::new(0, 0, 80, 5), &state, &colors);
});
assert!(
content.contains(expected_text),
"Expected '{}' in output for status {:?}",
expected_text,
status
);
}
info!("TEST PASS: test_render_header_all_daemon_statuses");
}
#[test]
fn test_render_header_shows_counts() {
init_test_logging();
info!("TEST START: test_render_header_shows_counts");
let state = TuiState {
workers: vec![
sample_worker("w1", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("w2", WorkerStatus::Healthy, CircuitState::Closed),
],
active_builds: vec![sample_active_build("b1", "cmd")],
..Default::default()
};
let content = render_to_string(80, 5, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_header(f, Rect::new(0, 0, 80, 5), &state, &colors);
});
assert!(content.contains("Workers: 2"));
assert!(content.contains("Builds: 1"));
info!("TEST PASS: test_render_header_shows_counts");
}
#[test]
fn test_render_footer_normal_mode_hints() {
init_test_logging();
info!("TEST START: test_render_footer_normal_mode_hints");
let state = TuiState::default();
let content = render_to_string(80, 5, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_footer(f, Rect::new(0, 0, 80, 5), &state, &colors);
});
assert!(content.contains("Quit"));
assert!(content.contains("Navigate"));
assert!(content.contains("Refresh"));
assert!(content.contains("Help"));
info!("TEST PASS: test_render_footer_normal_mode_hints");
}
#[test]
fn test_render_footer_filter_mode_hints() {
init_test_logging();
info!("TEST START: test_render_footer_filter_mode_hints");
let state = TuiState {
filter_mode: true,
..Default::default()
};
let content = render_to_string(80, 5, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_footer(f, Rect::new(0, 0, 80, 5), &state, &colors);
});
assert!(content.contains("Exit filter"));
assert!(content.contains("Apply"));
info!("TEST PASS: test_render_footer_filter_mode_hints");
}
#[test]
fn test_render_footer_log_view_hints() {
init_test_logging();
info!("TEST START: test_render_footer_log_view_hints");
let state = TuiState {
log_view: Some(LogViewState::default()),
..Default::default()
};
let content = render_to_string(80, 5, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_footer(f, Rect::new(0, 0, 80, 5), &state, &colors);
});
assert!(content.contains("Close logs"));
assert!(content.contains("Scroll"));
assert!(content.contains("Copy"));
info!("TEST PASS: test_render_footer_log_view_hints");
}
#[test]
fn test_render_logs_panel_empty_lines() {
init_test_logging();
info!("TEST START: test_render_logs_panel_empty_lines");
let log_view = LogViewState::default();
let state = TuiState {
log_view: Some(log_view),
selected_panel: Panel::Logs,
..Default::default()
};
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_logs_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(content.contains("Build Logs"));
info!("TEST PASS: test_render_logs_panel_empty_lines");
}
#[test]
fn test_render_logs_panel_auto_scroll_indicator() {
init_test_logging();
info!("TEST START: test_render_logs_panel_auto_scroll_indicator");
let mut log_view = LogViewState::default();
log_view.auto_scroll = true;
log_view.lines.push_back("test line".to_string());
let state = TuiState {
log_view: Some(log_view),
selected_panel: Panel::Logs,
..Default::default()
};
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_logs_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(content.contains("[AUTO]"));
info!("TEST PASS: test_render_logs_panel_auto_scroll_indicator");
}
#[test]
fn test_render_logs_panel_no_scroll_indicator_when_disabled() {
init_test_logging();
info!("TEST START: test_render_logs_panel_no_scroll_indicator_when_disabled");
let mut log_view = LogViewState::default();
log_view.auto_scroll = false;
log_view.lines.push_back("test line".to_string());
let state = TuiState {
log_view: Some(log_view),
selected_panel: Panel::Logs,
..Default::default()
};
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_logs_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(!content.contains("[AUTO]"));
info!("TEST PASS: test_render_logs_panel_no_scroll_indicator_when_disabled");
}
#[test]
fn test_render_at_narrow_width() {
init_test_logging();
info!("TEST START: test_render_at_narrow_width");
let state = TuiState {
workers: vec![sample_worker(
"w1",
WorkerStatus::Healthy,
CircuitState::Closed,
)],
active_builds: vec![sample_active_build("b1", "cargo build")],
..Default::default()
};
let result = std::panic::catch_unwind(|| {
let _ = render_to_string(40, 20, |f| render(f, &state));
});
assert!(result.is_ok());
info!("TEST PASS: test_render_at_narrow_width");
}
#[test]
fn test_render_at_wide_width() {
init_test_logging();
info!("TEST START: test_render_at_wide_width");
let state = TuiState {
workers: vec![sample_worker(
"w1",
WorkerStatus::Healthy,
CircuitState::Closed,
)],
active_builds: vec![sample_active_build("b1", "cargo build")],
..Default::default()
};
let result = std::panic::catch_unwind(|| {
let _ = render_to_string(200, 50, |f| render(f, &state));
});
assert!(result.is_ok());
info!("TEST PASS: test_render_at_wide_width");
}
#[test]
fn test_render_at_short_height() {
init_test_logging();
info!("TEST START: test_render_at_short_height");
let state = TuiState::default();
let result = std::panic::catch_unwind(|| {
let _ = render_to_string(80, 5, |f| render(f, &state));
});
assert!(result.is_ok());
info!("TEST PASS: test_render_at_short_height");
}
#[test]
fn test_render_main_content_shows_log_view_fullscreen() {
init_test_logging();
info!("TEST START: test_render_main_content_shows_log_view_fullscreen");
let mut log_view = LogViewState::default();
log_view.build_id = "test-build-123".to_string();
log_view.lines.push_back("Compiling test...".to_string());
let state = TuiState {
log_view: Some(log_view),
selected_panel: Panel::Logs,
..Default::default()
};
let content = render_to_string(80, 20, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_main_content(f, Rect::new(0, 0, 80, 20), &state, &colors);
});
assert!(content.contains("test-build-123"));
assert!(content.contains("Compiling test"));
info!("TEST PASS: test_render_main_content_shows_log_view_fullscreen");
}
#[test]
fn test_render_main_content_normal_layout() {
init_test_logging();
info!("TEST START: test_render_main_content_normal_layout");
let state = TuiState {
workers: vec![sample_worker(
"main-worker",
WorkerStatus::Healthy,
CircuitState::Closed,
)],
..Default::default()
};
let content = render_to_string(100, 30, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_main_content(f, Rect::new(0, 0, 100, 30), &state, &colors);
});
assert!(content.contains("main-worker"));
assert!(content.contains("Workers"));
info!("TEST PASS: test_render_main_content_normal_layout");
}
#[test]
fn test_selection_highlight_in_workers_panel() {
init_test_logging();
info!("TEST START: test_selection_highlight_in_workers_panel");
let state = TuiState {
selected_panel: Panel::Workers,
selected_index: 1,
workers: vec![
sample_worker("unselected", WorkerStatus::Healthy, CircuitState::Closed),
sample_worker("selected", WorkerStatus::Healthy, CircuitState::Closed),
],
..Default::default()
};
let content = render_to_string(60, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_workers_panel(f, Rect::new(0, 0, 60, 10), &state, &colors);
});
assert!(content.contains("unselected"));
assert!(content.contains("selected"));
info!("TEST PASS: test_selection_highlight_in_workers_panel");
}
#[test]
fn test_history_success_and_failure_icons() {
init_test_logging();
info!("TEST START: test_history_success_and_failure_icons");
let mut state = TuiState {
selected_panel: Panel::BuildHistory,
..Default::default()
};
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h1".to_string(),
command: "success cmd".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1200,
success: true,
exit_code: Some(0),
});
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h2".to_string(),
command: "failed cmd".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 500,
success: false,
exit_code: Some(1),
});
let content = render_to_string(80, 10, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_build_history_panel(f, Rect::new(0, 0, 80, 10), &state, &colors);
});
assert!(content.contains("success cmd"));
assert!(content.contains("failed cmd"));
info!("TEST PASS: test_history_success_and_failure_icons");
}
#[test]
fn test_history_local_worker_display() {
init_test_logging();
info!("TEST START: test_history_local_worker_display");
let mut state = TuiState {
selected_panel: Panel::BuildHistory,
..Default::default()
};
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "h1".to_string(),
command: "local build".to_string(),
worker: None, started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1200,
success: true,
exit_code: Some(0),
});
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_build_history_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(content.contains("local"));
info!("TEST PASS: test_history_local_worker_display");
}
#[test]
fn test_active_build_progress_percent_display() {
init_test_logging();
info!("TEST START: test_active_build_progress_percent_display");
let build = ActiveBuild {
id: "b1".to_string(),
command: "cargo build".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: Some(BuildProgress {
phase: "compiling".to_string(),
percent: Some(75),
current_file: None,
}),
status: BuildStatus::Compiling,
};
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: vec![build],
..Default::default()
};
let content = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
assert!(content.contains("75%"));
info!("TEST PASS: test_active_build_progress_percent_display");
}
#[test]
fn test_active_build_no_progress() {
init_test_logging();
info!("TEST START: test_active_build_no_progress");
let build = ActiveBuild {
id: "b1".to_string(),
command: "cargo build".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
progress: None,
status: BuildStatus::Pending,
};
let state = TuiState {
selected_panel: Panel::ActiveBuilds,
active_builds: vec![build],
..Default::default()
};
let result = std::panic::catch_unwind(|| {
let _ = render_to_string(80, 8, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_active_builds_panel(f, Rect::new(0, 0, 80, 8), &state, &colors);
});
});
assert!(result.is_ok());
info!("TEST PASS: test_active_build_no_progress");
}
#[test]
fn test_truncate_command_short() {
assert_eq!(truncate_command("cargo build", 40), "cargo build");
}
#[test]
fn test_truncate_command_preserves_release() {
let cmd = "cargo build --target x86_64-unknown-linux-gnu --release";
let result = truncate_command(cmd, 40);
assert!(result.len() <= 40);
assert!(
result.contains("--release"),
"Should preserve --release: {result}"
);
}
#[test]
fn test_truncate_command_preserves_package() {
let cmd = "cargo test --target x86_64-unknown-linux-gnu -p my-pkg";
let result = truncate_command(cmd, 35);
assert!(result.len() <= 35);
assert!(
result.contains("-p my-pkg") || result.ends_with("..."),
"Result: {result}"
);
}
#[test]
fn test_truncate_command_fallback() {
let cmd = "cargo build --target x86_64-unknown-linux-gnu --features full";
let result = truncate_command(cmd, 30);
assert!(result.len() <= 30);
assert!(result.ends_with("..."));
}
#[test]
fn test_indicator_color_maps_all_variants() {
init_test_logging();
info!("TEST START: test_indicator_color_maps_all_variants");
let colors = get_colors(false, ColorBlindMode::None);
assert_eq!(
indicator_color(StatusIndicator::Success, &colors),
colors.success
);
assert_eq!(
indicator_color(StatusIndicator::Error, &colors),
colors.error
);
assert_eq!(
indicator_color(StatusIndicator::Warning, &colors),
colors.warning
);
assert_eq!(indicator_color(StatusIndicator::Info, &colors), colors.info);
assert_eq!(
indicator_color(StatusIndicator::Pending, &colors),
colors.muted
);
assert_eq!(
indicator_color(StatusIndicator::InProgress, &colors),
colors.info
);
assert_eq!(
indicator_color(StatusIndicator::Disabled, &colors),
colors.muted
);
info!("TEST PASS: test_indicator_color_maps_all_variants");
}
#[test]
fn test_indicator_color_respects_color_blind_mode() {
init_test_logging();
info!("TEST START: test_indicator_color_respects_color_blind_mode");
let normal = get_colors(false, ColorBlindMode::None);
let deuter = get_colors(false, ColorBlindMode::Deuteranopia);
assert_ne!(
indicator_color(StatusIndicator::Success, &normal),
indicator_color(StatusIndicator::Success, &deuter),
);
info!("TEST PASS: test_indicator_color_respects_color_blind_mode");
}
#[test]
fn test_indicator_color_respects_high_contrast() {
init_test_logging();
info!("TEST START: test_indicator_color_respects_high_contrast");
let normal = get_colors(false, ColorBlindMode::None);
let high = get_colors(true, ColorBlindMode::None);
assert_ne!(
indicator_color(StatusIndicator::Success, &normal),
indicator_color(StatusIndicator::Success, &high),
);
info!("TEST PASS: test_indicator_color_respects_high_contrast");
}
#[test]
fn test_symbols_match_status_indicator() {
init_test_logging();
info!("TEST START: test_symbols_match_status_indicator");
assert_eq!(SYMBOLS.success, StatusIndicator::Success.symbol(&SYMBOLS));
assert_eq!(SYMBOLS.failure, StatusIndicator::Error.symbol(&SYMBOLS));
assert_eq!(SYMBOLS.warning, StatusIndicator::Warning.symbol(&SYMBOLS));
assert_eq!(
SYMBOLS.bullet_filled,
StatusIndicator::Info.symbol(&SYMBOLS)
);
assert_eq!(
SYMBOLS.bullet_empty,
StatusIndicator::Pending.symbol(&SYMBOLS)
);
assert_eq!(
SYMBOLS.bullet_half,
StatusIndicator::InProgress.symbol(&SYMBOLS)
);
assert_eq!(SYMBOLS.disabled, StatusIndicator::Disabled.symbol(&SYMBOLS));
info!("TEST PASS: test_symbols_match_status_indicator");
}
#[test]
fn test_build_history_uses_status_indicator_symbols() {
init_test_logging();
info!("TEST START: test_build_history_uses_status_indicator_symbols");
let mut state = TuiState {
selected_panel: Panel::BuildHistory,
..Default::default()
};
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "b1".to_string(),
command: "cargo build".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 1000,
success: true,
exit_code: Some(0),
});
state
.build_history
.push_back(crate::tui::state::HistoricalBuild {
id: "b2".to_string(),
command: "cargo test".to_string(),
worker: Some("w1".to_string()),
started_at: Utc::now(),
completed_at: Utc::now(),
duration_ms: 2000,
success: false,
exit_code: Some(1),
});
let content = render_to_string(80, 12, |f| {
let colors = get_colors(false, ColorBlindMode::None);
render_build_history_panel(f, Rect::new(0, 0, 80, 12), &state, &colors);
});
assert!(
content.contains(StatusIndicator::Success.symbol(&SYMBOLS)),
"Success builds should use StatusIndicator success symbol"
);
assert!(
content.contains(StatusIndicator::Error.symbol(&SYMBOLS)),
"Failed builds should use StatusIndicator error symbol"
);
info!("TEST PASS: test_build_history_uses_status_indicator_symbols");
}
#[test]
fn test_confirm_dialog_renders() {
init_test_logging();
info!("TEST START: test_confirm_dialog_renders");
use crate::tui::state::{ConfirmAction, ConfirmDialog};
let state = TuiState {
confirm_dialog: Some(ConfirmDialog {
title: "Drain worker 'css'?".into(),
message: "This will stop routing new\njobs to this worker.".into(),
action: ConfirmAction::DrainWorker("css".into()),
}),
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(
content.contains("Drain worker"),
"Dialog title must be visible"
);
assert!(
content.contains("stop routing"),
"Dialog message must be visible"
);
assert!(content.contains("Y"), "Yes option must be visible");
assert!(content.contains("N"), "No option must be visible");
info!("TEST PASS: test_confirm_dialog_renders");
}
#[test]
fn test_confirm_dialog_drain_all_renders() {
init_test_logging();
info!("TEST START: test_confirm_dialog_drain_all_renders");
use crate::tui::state::{ConfirmAction, ConfirmDialog};
let state = TuiState {
confirm_dialog: Some(ConfirmDialog {
title: "Drain ALL workers?".into(),
message: "This will stop routing new\njobs to every worker.".into(),
action: ConfirmAction::DrainAllWorkers,
}),
..Default::default()
};
let content = render_to_string(80, 24, |f| render(f, &state));
assert!(
content.contains("Drain ALL"),
"Dialog title must be visible"
);
info!("TEST PASS: test_confirm_dialog_drain_all_renders");
}
#[test]
fn test_help_overlay_documents_worker_actions() {
init_test_logging();
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
assert!(
content.contains("Worker Actions"),
"Worker Actions section must exist"
);
assert!(
content.contains("Drain ALL"),
"Drain ALL shortcut must be documented"
);
assert!(
content.contains("Enable ALL"),
"Enable ALL shortcut must be documented"
);
}
#[test]
fn test_help_overlay_documents_build_actions() {
init_test_logging();
let state = TuiState {
show_help: true,
..Default::default()
};
let content = render_to_string(80, 50, |f| render(f, &state));
assert!(
content.contains("Build Actions"),
"Build Actions section must exist"
);
assert!(
content.contains("Cancel selected"),
"Cancel build shortcut must be documented"
);
assert!(
content.contains("Force kill"),
"Force kill shortcut must be documented"
);
}
use crate::tui::test_data;
fn remediation_state(view: rch_common::remediation_view::RemediationView) -> TuiState {
let mut state = TuiState::new();
state.show_remediation = true;
state.remediation = Some(view);
state
}
fn render_remediation(state: &TuiState) -> String {
render_to_string(90, 60, |f| render(f, state))
}
#[test]
fn test_remediation_overlay_healthy() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_healthy());
let content = render_remediation(&state);
assert!(content.contains("Remediation"), "overlay title present");
assert!(content.contains("Overall posture"), "posture line present");
assert!(
content.contains("Desired Fleet"),
"desired fleet band present"
);
assert!(
content.contains("Live Eligibility"),
"eligibility band present"
);
assert!(
content.contains("Admissible Workers"),
"admissible band present"
);
assert!(content.contains("Proof Queue"), "proof band present");
assert!(
content.contains("Telemetry Freshness"),
"telemetry band present"
);
assert!(
!content.contains("[ACTION]"),
"a healthy fleet needs no operator action"
);
}
#[test]
fn test_remediation_overlay_degraded() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_degraded());
let content = render_remediation(&state);
assert!(
content.contains("[HEALING]"),
"degraded fleet is self-healing"
);
assert!(
content.contains("bypassed"),
"bypass is surfaced in the eligibility band"
);
}
#[test]
fn test_remediation_overlay_no_admissible_workers() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_no_admissible());
let content = render_remediation(&state);
assert!(
content.contains("[ACTION]"),
"no admissible workers requires operator action"
);
assert!(content.contains("Admissible Workers"));
}
#[test]
fn test_remediation_overlay_proof_queued() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_proof_queued());
let content = render_remediation(&state);
assert!(content.contains("Proof Queue"));
assert!(
content.contains("[HEALING]"),
"an active proof queue is self-healing"
);
}
#[test]
fn test_remediation_overlay_disk_pressure() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_disk_pressure());
let content = render_remediation(&state);
assert!(content.contains("Disk Pressure"));
assert!(
content.contains("[ACTION]"),
"critical disk pressure without reclaim requires operator action"
);
}
#[test]
fn test_remediation_overlay_stale_telemetry() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_stale_telemetry());
let content = render_remediation(&state);
assert!(content.contains("Telemetry Freshness"));
assert!(
content.contains("[HEALING]"),
"stale telemetry self-corrects"
);
}
#[test]
fn test_remediation_overlay_auto_rejoin_pending() {
init_test_logging();
let state = remediation_state(test_data::mock_remediation_view_auto_rejoin_pending());
let content = render_remediation(&state);
assert!(content.contains("[HEALING]"), "auto-rejoin is self-healing");
assert!(
content.contains("canary") || content.contains("bypassed"),
"auto-rejoin state is surfaced"
);
}
#[test]
fn test_remediation_overlay_unavailable_when_none() {
init_test_logging();
let mut state = TuiState::new();
state.show_remediation = true;
state.remediation = None;
let content = render_remediation(&state);
assert!(
content.contains("Remediation view unavailable"),
"absent snapshot renders an explicit unavailable message"
);
}
#[test]
fn test_remediation_overlay_hidden_when_toggled_off() {
init_test_logging();
let mut state = TuiState::new();
state.show_remediation = false;
state.remediation = Some(test_data::mock_remediation_view_healthy());
let content = render_remediation(&state);
assert!(
!content.contains("Overall posture"),
"overlay must not render when toggled off"
);
}
}