use super::*;
#[test]
fn card_headers_have_one_padding_consistent_status_marks_and_bullet_separators() {
let theme = crate::tui::theme::MissionControlTheme::default();
let states = [
(
bash_card_state(
"header-running",
"bash",
ActivityStatus::Running,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({}),
"",
),
"● Bash",
crate::tui::theme::MissionControlTheme::default()
.activity_status_color(ActivityStatus::Running),
Modifier::BOLD,
),
(
bash_card_state(
"header-success",
"bash",
ActivityStatus::Success,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({"exit_code":0}),
"ok",
),
"✓ Bash • exit 0",
theme.activity_status_color(ActivityStatus::Success),
Modifier::empty(),
),
(
bash_card_state(
"header-failed",
"bash",
ActivityStatus::Failed,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({"exit_code":1}),
"failed",
),
"✗ Bash • exit 1",
theme.activity_status_color(ActivityStatus::Failed),
Modifier::BOLD,
),
(
bash_card_state(
"header-canceled",
"bash",
ActivityStatus::Canceled,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({}),
"canceled",
),
"⊘ Bash • canceled",
crate::tui::theme::MissionControlTheme::default()
.activity_status_color(ActivityStatus::Canceled),
Modifier::DIM,
),
];
for (state, expected, status_color, status_modifier) in states {
let header = &visual_lines(&state, 80)[1];
let rendered = line_text(&header.line);
assert!(
rendered.starts_with(format!(" {expected}").as_str()),
"{rendered:?}"
);
assert!(!header.copy_text.contains(" · "), "{}", header.copy_text);
assert_eq!(header.visual_copy_column_offset, 1);
assert_eq!(UnicodeWidthStr::width(rendered.as_str()), 80);
assert_eq!(header.line.spans[1].style.fg, Some(status_color));
assert_eq!(header.line.spans[1].style.add_modifier, status_modifier);
let title = header
.line
.spans
.iter()
.find(|span| span.content.contains("Bash"))
.expect("Bash title span");
assert_eq!(title.style.fg, Some(theme.text_accent()));
assert!(title.style.add_modifier.contains(Modifier::BOLD));
assert!(
header
.line
.spans
.iter()
.all(|span| span.style.bg == Some(theme.surface_panel_alt()))
);
}
let generic = linked_tool_state(
"header-generic",
"dynamic_tool",
"dynamic_tool",
ActivityStatus::Success,
Vec::new(),
"ok",
);
let generic_header = &visual_lines(&generic, 80)[1];
assert!(line_text(&generic_header.line).starts_with(" ✓ Tool • dynamic_tool"));
let generic_title = generic_header
.line
.spans
.iter()
.find(|span| span.content.contains("Tool"))
.expect("generic tool title span");
assert_eq!(generic_title.style.fg, Some(theme.text_accent()));
assert!(generic_title.style.add_modifier.contains(Modifier::BOLD));
let mut subagent_card = MissionControlState::default();
append_subagent_card_batch(
&mut subagent_card,
"header-subagent_card",
&[ActivityStatus::Success],
);
assert!(
line_text(&visual_lines(&subagent_card, 80)[1].line)
.starts_with(" Subagents • 1/1 settled")
);
}
#[test]
fn card_headers_preserve_status_contract_at_tiny_widths() {
let state = bash_card_state(
"tiny-running-header",
"bash",
ActivityStatus::Running,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({}),
"",
);
for (width, expected) in [(1, " "), (2, " ●")] {
let header = &visual_lines(&state, width)[1];
assert_eq!(line_text(&header.line), expected);
assert_eq!(
UnicodeWidthStr::width(line_text(&header.line).as_str()),
width as usize
);
assert_eq!(header.copy_text, "● Bash");
assert_eq!(header.visual_copy_column_offset, 1);
}
}
#[test]
fn legacy_generic_tool_marks_normalize_to_canonical_headers() {
for (legacy, expected_status, expected_title, expected_header) in [
(
"⟳ alpha • running",
TranscriptCardStatus::Running,
"alpha",
"● Tool • alpha",
),
(
"○ beta • queued",
TranscriptCardStatus::Running,
"beta",
"● Tool • beta",
),
(
"⊘ gamma • canceled",
TranscriptCardStatus::Canceled,
"gamma • canceled",
"⊘ Tool • gamma • canceled",
),
] {
let mut state = MissionControlState::default();
state.transcript.push_back(format!("tool: {legacy}"));
let card = &project_cards(&state)[0];
assert_eq!(card.status, expected_status);
assert_eq!(card.title, expected_title);
assert!(
line_text(&visual_lines(&state, 80)[1].line)
.starts_with(format!(" {expected_header}").as_str()),
"{legacy}"
);
}
}
#[test]
fn error_and_markless_semantic_headers_keep_their_exceptions() {
let mut error = MissionControlState::default();
error
.transcript
.push_back("error: failed safely".to_string());
let error_header = &visual_lines(&error, 80)[1];
assert!(line_text(&error_header.line).starts_with(" ✗ Error"));
assert!(
error_header
.line
.spans
.iter()
.all(|span| span.style.bg == Some(MissionControlTheme::default().surface_error()))
);
for entry in ["assistant: answer", "thinking: summary"] {
let mut state = MissionControlState::default();
state.transcript.push_back(entry.to_string());
let header = &visual_lines(&state, 80)[1];
assert!(!header.copy_text.starts_with(['✓', '✗', '●', '⊘']));
assert!(matches!(header.copy_text.as_str(), "Agent" | "Reasoning"));
}
}
#[test]
fn canceled_read_and_generic_tool_use_one_canonical_suffix() {
let read = read_card_state(
"canceled-read-header",
ActivityStatus::Canceled,
serde_json::json!({"paths":["src/lib.rs", "src/main.rs"]}),
serde_json::json!({"files":2}),
"prompt canceled",
);
let read_card = &project_cards(&read)[0];
assert_eq!(read_card.title, "Read • 2 resources • canceled");
assert_eq!(
read_card.body,
"src/lib.rs\nsrc/main.rs\nCanceled before completion"
);
assert!(
line_text(&visual_lines(&read, 80)[1].line).starts_with(" ⊘ Read • 2 resources • canceled")
);
let tool = linked_tool_state(
"canceled-generic-header",
"dynamic_tool",
"dynamic_tool",
ActivityStatus::Canceled,
Vec::new(),
"canceled",
);
let tool_header = line_text(&visual_lines(&tool, 80)[1].line);
assert!(tool_header.starts_with(" ⊘ Tool • dynamic_tool • canceled"));
assert_eq!(tool_header.matches("canceled").count(), 1);
}
#[test]
fn non_user_headers_have_three_styled_rows_without_copy_or_hit_padding() {
let mut states = Vec::new();
for entry in [
"assistant: answer",
"thinking: summary",
"error: failed safely",
"tool: ✓ dynamic_tool • done",
"system: notice",
] {
let mut state = MissionControlState::default();
state.transcript.push_back(entry.to_string());
states.push(state);
}
states.push(bash_card_state(
"three-row-bash",
"bash",
ActivityStatus::Running,
serde_json::json!({"command":"cargo check"}),
serde_json::json!({}),
"",
));
let mut subagents = MissionControlState::default();
append_subagent_card_batch(
&mut subagents,
"three-row-subagents",
&[ActivityStatus::Success],
);
states.push(subagents);
for state in states {
for width in [1, 2, 12, 80] {
let lines = visual_lines(&state, width);
let header = &lines[1];
assert!(header.copyable);
assert!(!header.copy_text.is_empty());
let background = header.line.spans[0].style.bg;
for row in [0, 2] {
let padding = &lines[row];
assert_eq!(line_text(&padding.line), " ".repeat(usize::from(width)));
assert!(
padding
.line
.spans
.iter()
.all(|span| span.style.bg == background)
);
assert!(!padding.copyable);
assert!(padding.copy_text.is_empty());
assert!(padding.copy_byte_range.is_none());
assert!(padding.hit_regions.is_empty());
assert!(padding.status_indicators.is_empty());
assert_eq!(visual_line_rows(padding, width), 1);
}
}
}
}