use super::*;
use crate::output::{ActivityEvent, ActivityKind, ActivityMetadata, ActivityStatus, OutputEvent};
fn roles(state: &MissionControlState) -> Vec<TranscriptCardRole> {
project_cards(state)
.into_iter()
.map(|card| card.role)
.collect()
}
fn linked_tool_state(
id: &str,
tool_name: &str,
activity_label: &str,
status: ActivityStatus,
fields: Vec<(String, String)>,
preview: &str,
) -> MissionControlState {
let mut state = MissionControlState::default();
let id = ActivityId::new(id);
state.transcript.push_back(format!(
"tool: {} {activity_label}",
status_mark(transcript_status_from_activity(status))
));
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: tool_name.to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: if tool_name == "subagents" {
ActivityKind::SubagentBatch
} else {
ActivityKind::Tool
},
status,
metadata: ActivityMetadata {
label: activity_label.to_string(),
detail: None,
fields,
},
});
state.apply_activity_event(ActivityEvent::Delta {
id,
preview: preview.to_string(),
});
state
}
#[test]
fn projects_core_transcript_roles_and_stable_ids() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "prompt".into(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "summary".into(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer".into(),
});
state.record_error_transcript("boom");
let cards = project_cards(&state);
assert_eq!(
cards.iter().map(|card| card.role).collect::<Vec<_>>(),
vec![
TranscriptCardRole::UserPrompt,
TranscriptCardRole::Reasoning,
TranscriptCardRole::Assistant,
TranscriptCardRole::Error,
]
);
assert_eq!(cards[0].id.entry_index, 0);
assert_eq!(cards[2].id.entry_index, 2);
assert_eq!(cards[0].title, "👤You");
assert_eq!(cards[1].title, "🧠Reasoning Summary");
assert_eq!(cards[2].title, "🤖Assistant");
}
#[test]
fn projects_bash_command_as_bash_card_not_user_turn() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::BashCommand {
command: "printf hi".into(),
});
let cards = project_cards(&state);
assert_eq!(cards.len(), 1);
assert_eq!(cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cards[0].title, "⌘ Bash");
assert_eq!(cards[0].body, "printf hi");
}
#[test]
fn bash_mode_tool_card_uses_full_activity_preview_inline() {
let mut state = MissionControlState::default();
let id = ActivityId::new("bash_mode_1");
state.apply_output_event(&OutputEvent::BashCommand {
command: "printf 'one\\ntwo\\nthree\\nfour\\nfive\\n'".into(),
});
state
.transcript
.push_back("tool: ✓ bash • exit 0".to_string());
state.transcript_activity_links.insert(
1,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "bash".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Success,
metadata: ActivityMetadata::new("bash"),
});
state.apply_activity_event(ActivityEvent::Delta {
id,
preview:
"STATUS: success\nCOMMAND: printf\nEXIT_CODE: 0\nOUTPUT:\none\ntwo\nthree\nfour\nfive\n"
.to_string(),
});
let cards = project_cards(&state);
assert_eq!(cards.len(), 2);
assert_eq!(cards[0].role, TranscriptCardRole::Bash);
assert_eq!(cards[1].role, TranscriptCardRole::Tool);
assert_eq!(cards[1].title, BASH_MODE_RESPONSE_TITLE);
assert!(cards[1].body.contains("OUTPUT:"), "{}", cards[1].body);
assert!(cards[1].body.contains("five"), "{}", cards[1].body);
assert!(cards[1].body.lines().count() > TOOL_PREVIEW_LINES);
let visual = state.transcript_visual_text();
assert!(visual.contains("⌘ Bash\n printf"), "{visual}");
assert!(
visual.contains("🔧Tool: Bash Mode Response\n STATUS: success"),
"{visual}"
);
assert!(visual.contains("OUTPUT:\n one"), "{visual}");
assert!(visual.contains("five"), "{visual}");
}
#[test]
fn normal_bash_tool_card_keeps_compact_preview() {
let state = linked_tool_state(
"call_bash",
"bash",
"bash printf",
ActivityStatus::Success,
Vec::new(),
"STATUS: success\nCOMMAND: printf\nEXIT_CODE: 0\nOUTPUT:\none\ntwo\nthree\nfour\nfive\n",
);
let cards = project_cards(&state);
assert_eq!(cards[0].title, "bash");
assert_eq!(cards[0].body.lines().count(), TOOL_PREVIEW_LINES);
assert!(cards[0].body.contains("OUTPUT:"), "{}", cards[0].body);
assert!(!cards[0].body.contains("one"), "{}", cards[0].body);
let visual = state.transcript_visual_text();
assert!(
visual.contains("🔧Tool: bash\n STATUS: success"),
"{visual}"
);
assert!(!visual.contains("five"), "{visual}");
}
#[test]
fn projects_empty_state() {
let state = MissionControlState::default();
let cards = project_cards(&state);
assert_eq!(cards.len(), 1);
assert_eq!(cards[0].role, TranscriptCardRole::Empty);
assert!(cards[0].body.contains("Transcript will appear here"));
}
#[test]
fn maps_local_diagnostics_sessions_canceled_and_legacy_safely() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::Diagnostic {
level: "info".into(),
message: "note".into(),
});
state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".into(),
message: "careful".into(),
});
state.apply_output_event(&OutputEvent::Diagnostic {
level: "error".into(),
message: "bad".into(),
});
state.record_canceled_transcript("prompt text");
state.transcript.push_back("session • restored".to_string());
state
.transcript
.push_back("weird legacy prefix".to_string());
state.invalidate_transcript_cache();
let cards = project_cards(&state);
assert_eq!(
cards.iter().map(|card| card.role).collect::<Vec<_>>(),
vec![
TranscriptCardRole::Diagnostic,
TranscriptCardRole::Diagnostic,
TranscriptCardRole::Error,
TranscriptCardRole::Diagnostic,
TranscriptCardRole::Session,
TranscriptCardRole::Diagnostic,
]
);
assert_eq!(cards[3].title, "Canceled");
assert_eq!(cards[3].body, "prompt text");
assert_eq!(cards[5].title, "Legacy diagnostic");
}
#[test]
fn maps_hook_diagnostic_without_raw_prefix_in_body() {
let mut state = MissionControlState::default();
state.transcript.push_back("hook: tool warning".to_string());
let cards = project_cards(&state);
assert_eq!(cards[0].role, TranscriptCardRole::Diagnostic);
assert_eq!(cards[0].title, "Hook diagnostic");
assert_eq!(cards[0].body, "tool warning");
assert!(!cards[0].body.contains("hook:"));
}
#[test]
fn detects_subagents_and_child_rows_from_activity() {
let mut state = MissionControlState::default();
let parent = ActivityId::new("call_parallel");
state
.transcript
.push_back("tool: ⟳ subagents • running".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: parent.clone(),
tool_name: "subagents".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: parent.clone(),
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("subagents • 2 tasks"),
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_parallel/g1"),
parent_id: Some(parent.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Success,
metadata: ActivityMetadata::new("g1 identity rust-dev"),
});
let cards = project_cards(&state);
assert_eq!(cards[0].role, TranscriptCardRole::SubagentBatch);
assert_eq!(cards[0].children.len(), 1);
assert_eq!(cards[0].children[0].status, TranscriptCardStatus::Success);
}
#[test]
fn squad_card_parses_intent_led_rows_and_aggregate_progress() {
let mut state = MissionControlState::default();
let parent = ActivityId::new("call_parallel");
state
.transcript
.push_back("tool: ⟳ subagents • running".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: parent.clone(),
tool_name: "subagents".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: parent.clone(),
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata {
label: "subagents • 3 tasks".to_string(),
detail: None,
fields: vec![("tasks".to_string(), "3".to_string())],
},
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_parallel/g1"),
parent_id: Some(parent.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Success,
metadata: ActivityMetadata {
label: "g1 • depth 2 • implement the squad card".to_string(),
detail: None,
fields: vec![("identity".to_string(), "rust-dev".to_string())],
},
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_parallel/g2"),
parent_id: Some(parent.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata {
label: "g2 • depth 2 • investigate theme colors".to_string(),
detail: None,
fields: vec![],
},
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_parallel/g3"),
parent_id: Some(parent.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Failed,
metadata: ActivityMetadata {
label: "g3 • depth 2 • audit cache invalidation".to_string(),
detail: None,
fields: vec![],
},
});
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("call_parallel/g3"),
preview: "provider timeout after 30s".to_string(),
});
let cards = project_cards(&state);
let card = &cards[0];
assert_eq!(card.children.len(), 3);
assert_eq!(card.children[0].task_id, "g1");
assert_eq!(card.children[0].intent, "implement the squad card");
assert_eq!(card.children[0].identity.as_deref(), Some("rust-dev"));
assert_eq!(card.children[1].task_id, "g2");
assert_eq!(card.children[1].intent, "investigate theme colors");
assert!(card.children[1].identity.is_none());
let squad = card.squad.as_ref().expect("squad summary");
assert_eq!(squad.total, 3);
assert_eq!(squad.done, 1);
assert_eq!(squad.failed, 1);
assert_eq!(squad.running, 1);
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let rendered = wide_lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("🛰 Squad"), "{rendered}");
assert!(rendered.contains("1/3"), "{rendered}");
assert!(rendered.contains("⟳1"), "{rendered}");
assert!(rendered.contains("✗1"), "{rendered}");
assert!(rendered.contains("implement the squad card"), "{rendered}");
assert!(rendered.contains("investigate theme colors"), "{rendered}");
assert!(rendered.contains("audit cache invalidation"), "{rendered}");
assert!(
rendered.contains("provider timeout after 30s"),
"{rendered}"
);
assert!(!rendered.contains("depth"), "{rendered}");
assert!(!rendered.contains("TASK_COUNT"), "{rendered}");
}
#[test]
fn squad_card_handles_no_separator_label_shape() {
let mut state = MissionControlState::default();
let parent = ActivityId::new("call_parallel");
state
.transcript
.push_back("tool: ⟳ subagents • running".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: parent.clone(),
tool_name: "subagents".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: parent.clone(),
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("subagents • 1 tasks"),
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_parallel/g1"),
parent_id: Some(parent),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
let cards = project_cards(&state);
let card = &cards[0];
assert_eq!(card.children.len(), 1);
assert_eq!(card.children[0].task_id, "g1");
assert_eq!(card.children[0].intent, "inspect");
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let rendered = wide_lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("g1"), "{rendered}");
assert!(rendered.contains("inspect"), "{rendered}");
}
#[test]
fn tool_card_headers_show_tool_label_while_body_keeps_metadata() {
let mut state = linked_tool_state(
"call_read",
"read",
"read src/tui/transcript_cards.rs",
ActivityStatus::Success,
vec![(
"path".to_string(),
"src/tui/transcript_cards.rs".to_string(),
)],
"STATUS: success\nPATH: src/tui/transcript_cards.rs\nOUTPUT:\ncontents",
);
let cards = project_cards(&state);
assert_eq!(cards[0].title, "read");
assert!(cards[0].body.contains("STATUS: success"));
assert!(cards[0].body.contains("PATH: src/tui/transcript_cards.rs"));
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let wide_header = line_text(&wide_lines[0].line);
assert!(wide_header.starts_with("┌─ 🔧Tool: read"), "{wide_header}");
assert!(!wide_header.starts_with("┌─ read"), "{wide_header}");
assert!(!wide_header.contains('✓'), "{wide_header}");
assert!(
!wide_header.contains("src/tui/transcript_cards.rs"),
"{wide_header}"
);
assert!(!wide_header.contains("STATUS"), "{wide_header}");
let compact_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH - 1);
let compact_header = line_text(&compact_lines[0].line);
assert_eq!(compact_header, "🔧Tool: read");
let visual = state.transcript_visual_text();
assert!(
visual.contains("🔧Tool: read\n STATUS: success"),
"{visual}"
);
assert!(
visual.contains("PATH: src/tui/transcript_cards.rs"),
"{visual}"
);
assert!(
!visual.contains("🔧Tool: read src/tui/transcript_cards.rs\n STATUS"),
"{visual}"
);
state.start_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::input::selection::TextPosition::new(0, 0, 0),
);
state.update_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::input::selection::TextPosition::new(visual.len(), 0, visual.len()),
);
let copied = state
.finish_selection(
crate::tui::layout::TuiPane::Transcript,
CARD_LAYOUT_MIN_WIDTH,
)
.expect("copied text");
assert!(
copied.contains("🔧Tool: read\n STATUS: success"),
"{copied}"
);
assert!(!copied.contains('┌'), "{copied}");
}
#[test]
fn bash_tool_card_header_omits_metadata_kept_in_body() {
let state = linked_tool_state(
"call_bash",
"bash",
"bash cargo check",
ActivityStatus::Success,
vec![("exit_code".to_string(), "0".to_string())],
"STATUS: success\nCOMMAND: cargo check\nEXIT_CODE: 0\nOUTPUT:\nok",
);
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let wide_header = line_text(&wide_lines[0].line);
assert!(wide_header.starts_with("┌─ 🔧Tool: bash"), "{wide_header}");
assert!(!wide_header.starts_with("┌─ bash"), "{wide_header}");
assert!(!wide_header.contains("cargo check"), "{wide_header}");
let visual = state.transcript_visual_text();
assert!(
visual.contains("🔧Tool: bash\n STATUS: success"),
"{visual}"
);
assert!(visual.contains("COMMAND: cargo check"), "{visual}");
}
#[test]
fn subagents_card_header_uses_dispatch_name_without_task_metadata() {
let state = linked_tool_state(
"call_parallel",
"subagents",
"subagents • 2 tasks",
ActivityStatus::Running,
vec![("tasks".to_string(), "2".to_string())],
"STATUS: running\nTASK_COUNT: 2\nTASK_IDS: g1, g2\nsummary: total=2 completed=0 failed=0",
);
let cards = project_cards(&state);
assert_eq!(cards[0].role, TranscriptCardRole::SubagentBatch);
assert!(cards[0].metadata.is_empty());
let squad = cards[0]
.squad
.as_ref()
.expect("subagent batch carries squad summary");
assert_eq!(squad.total, 2);
assert_eq!(squad.done, 0);
assert_eq!(squad.failed, 0);
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let wide_header = line_text(&wide_lines[0].line);
assert!(wide_header.starts_with("┌─ 🛰 Squad ──"), "{wide_header}");
assert!(wide_header.contains("0/2"), "{wide_header}");
assert!(!wide_header.contains("TASK_COUNT"), "{wide_header}");
assert!(!wide_header.contains('🔧'), "{wide_header}");
let rendered = wide_lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(ACTIVITY_BUTTON_LABEL), "{rendered}");
assert!(!rendered.contains("TASK_COUNT"), "{rendered}");
assert!(!rendered.contains("TASK_IDS"), "{rendered}");
}
#[test]
fn tool_and_subagent_cards_expose_activity_button_without_copy_text() {
let mut state = MissionControlState::default();
let tool_id = ActivityId::new("call_read");
let batch_id = ActivityId::new("call_parallel");
state
.transcript
.push_back("tool: ✓ read • done".to_string());
state
.transcript
.push_back("tool: ⟳ subagents • running".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: tool_id.clone(),
tool_name: "read".to_string(),
},
);
state.transcript_activity_links.insert(
1,
TranscriptActivityLink {
activity_id: batch_id.clone(),
tool_name: "subagents".to_string(),
},
);
state.invalidate_transcript_cache();
for width in [CARD_LAYOUT_MIN_WIDTH - 1, CARD_LAYOUT_MIN_WIDTH] {
let lines = visual_lines(&state, width);
let rendered = lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(
rendered.matches(ACTIVITY_BUTTON_LABEL).count(),
2,
"width {width}"
);
assert!(!rendered.contains("[activity]"), "width {width}");
assert!(
lines.iter().any(|line| line
.activity_button
.as_ref()
.map(|button| &button.activity_id)
== Some(&tool_id)),
"tool button missing at width {width}"
);
assert!(
lines.iter().any(|line| line
.activity_button
.as_ref()
.map(|button| &button.activity_id)
== Some(&batch_id)),
"subagent batch button missing at width {width}"
);
}
let copy_text = visual_text(&state);
assert!(!copy_text.contains(ACTIVITY_BUTTON_LABEL), "{copy_text}");
assert!(!copy_text.contains("[activity]"), "{copy_text}");
}
#[test]
fn wide_activity_button_renders_on_bottom_right_border_line() {
let mut state = MissionControlState::default();
let id = ActivityId::new("call_read");
state
.transcript
.push_back("tool: ✓ read • done".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "read".to_string(),
},
);
state.invalidate_transcript_cache();
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let wide_button_line = wide_lines
.iter()
.find(|line| line_text(&line.line).contains(ACTIVITY_BUTTON_LABEL))
.expect("wide activity button");
let wide_text = line_text(&wide_button_line.line);
let label_byte_start = wide_text
.find(ACTIVITY_BUTTON_LABEL)
.expect("right-aligned activity label");
let label_column = UnicodeWidthStr::width(&wide_text[..label_byte_start]);
assert_eq!(
label_column,
usize::from(CARD_LAYOUT_MIN_WIDTH) - 1 - ACTIVITY_BUTTON_LABEL.len(),
"{wide_text}"
);
assert!(wide_text.starts_with('└'), "{wide_text}");
assert!(
wide_text.ends_with(&format!("{ACTIVITY_BUTTON_LABEL}┘")),
"{wide_text}"
);
assert_eq!(
wide_text.chars().count(),
usize::from(CARD_LAYOUT_MIN_WIDTH)
);
assert_eq!(
wide_button_line.activity_button.as_ref().map(|button| (
button.activity_id.clone(),
button.start_column,
button.end_column,
)),
Some((
id.clone(),
(usize::from(CARD_LAYOUT_MIN_WIDTH) - 1 - ACTIVITY_BUTTON_LABEL.len()) as u16,
(usize::from(CARD_LAYOUT_MIN_WIDTH) - 1) as u16,
))
);
assert!(!wide_text.contains("[activity]"));
let compact_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH - 1);
let compact_text = compact_lines
.iter()
.map(|line| line_text(&line.line))
.find(|text| text.contains(ACTIVITY_BUTTON_LABEL))
.expect("compact activity button");
assert!(
compact_text.starts_with(&format!(" {ACTIVITY_BUTTON_LABEL}")),
"{compact_text}"
);
assert!(!compact_text.contains('└'));
}
#[test]
fn wide_assistant_card_border_spans_use_header_style_without_coloring_body() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "plain answer".into(),
});
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let header_style = wide_lines[0].line.spans[0].style;
let body_line = wide_lines
.iter()
.find(|line| line_text(&line.line).starts_with('│'))
.expect("assistant body line");
let bottom_line = wide_lines.last().expect("assistant bottom line");
assert_eq!(body_line.line.spans.first().unwrap().style, header_style);
assert_eq!(body_line.line.spans.last().unwrap().style, header_style);
assert_eq!(bottom_line.line.spans.first().unwrap().style, header_style);
let body_span = body_line
.line
.spans
.iter()
.find(|span| span.content.as_ref().contains("plain answer"))
.expect("assistant body span");
assert_ne!(body_span.style, header_style);
assert_eq!(body_span.style, MATRIX_GREEN.pane());
}
#[test]
fn wide_tool_card_border_spans_match_header_and_keep_button_hit_target() {
let state = linked_tool_state(
"call_read",
"read",
"read src/lib.rs",
ActivityStatus::Success,
Vec::new(),
"STATUS: success\nOUTPUT:\ncontents",
);
let wide_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let header_style = wide_lines[0].line.spans[0].style;
let body_line = wide_lines
.iter()
.find(|line| line_text(&line.line).contains("STATUS: success"))
.expect("tool body line");
assert_eq!(body_line.line.spans.first().unwrap().style, header_style);
assert_eq!(body_line.line.spans.last().unwrap().style, header_style);
let body_span = body_line
.line
.spans
.iter()
.find(|span| span.content.as_ref().contains("STATUS: success"))
.expect("tool body span");
assert_ne!(body_span.style, header_style);
assert_eq!(body_span.style, MATRIX_GREEN.transcript_tool_body());
let button_line = wide_lines
.iter()
.find(|line| line.activity_button.is_some())
.expect("activity button line");
assert_eq!(button_line.line.spans.first().unwrap().style, header_style);
assert_eq!(button_line.line.spans.last().unwrap().style, header_style);
let button_span = button_line
.line
.spans
.iter()
.find(|span| span.content.as_ref() == ACTIVITY_BUTTON_LABEL)
.expect("button span");
assert_ne!(button_span.style, header_style);
assert!(
button_span
.style
.add_modifier
.contains(Modifier::UNDERLINED)
);
assert_eq!(
button_line.activity_button.as_ref().map(|button| (
&button.activity_id,
button.start_column,
button.end_column
)),
Some((
&ActivityId::new("call_read"),
(usize::from(CARD_LAYOUT_MIN_WIDTH) - 1 - ACTIVITY_BUTTON_LABEL.len()) as u16,
(usize::from(CARD_LAYOUT_MIN_WIDTH) - 1) as u16,
))
);
}
#[test]
fn activity_button_hit_testing_only_matches_button_columns() {
let mut state = MissionControlState::default();
let id = ActivityId::new("call_read");
state
.transcript
.push_back("tool: ✓ read • done".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "read".to_string(),
},
);
state.invalidate_transcript_cache();
for width in [CARD_LAYOUT_MIN_WIDTH - 1, CARD_LAYOUT_MIN_WIDTH] {
let lines = visual_lines(&state, width);
let (row, button) = lines
.iter()
.enumerate()
.find_map(|(row, line)| line.activity_button.as_ref().map(|button| (row, button)))
.expect("activity button");
assert_eq!(
activity_button_for_visual_cell(&state, row, usize::from(button.start_column), width,),
Some(id.clone()),
"width {width}"
);
assert_eq!(
activity_button_for_visual_cell(
&state,
row,
usize::from(button.start_column.saturating_sub(1)),
width,
),
None,
"width {width}"
);
assert_eq!(
activity_button_for_visual_cell(&state, row, usize::from(button.end_column), width),
None,
"width {width}"
);
}
}
#[test]
fn tool_card_without_activity_link_has_no_activity_button() {
let state = MissionControlState {
transcript: vec!["tool: ✓ read • done".to_string()].into(),
..Default::default()
};
let rendered = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(!rendered.contains(ACTIVITY_BUTTON_LABEL));
assert!(!rendered.contains("[activity]"));
assert!(
visual_lines(&state, CARD_LAYOUT_MIN_WIDTH)
.iter()
.all(|line| line.activity_button.is_none())
);
}
#[test]
fn visual_text_excludes_card_decoration_and_keeps_labels() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello".into(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "**world**".into(),
});
let text = visual_text(&state);
assert!(text.contains("👤You\n hello"));
assert!(text.contains("🤖Assistant\n world"));
assert!(!text.contains('┌'));
assert!(!text.contains('│'));
}
#[test]
fn tui_perf_projected_card_counter_only_updates_when_enabled() {
let mut disabled = MissionControlState::default();
disabled.transcript.push_back("assistant: off".to_string());
let _ = project_cards(&disabled);
assert_eq!(disabled.perf.latest_snapshot(), None);
let mut enabled = MissionControlState {
perf: crate::tui::perf::TuiPerfCounters::with_enabled(true),
..Default::default()
};
enabled.transcript.push_back("assistant: on".to_string());
enabled.perf.reset_projected_cards();
let _ = project_cards(&enabled);
enabled.perf.record_draw_us(1);
let snapshot = enabled.perf.latest_snapshot().unwrap();
assert_eq!(snapshot.projected_cards, 1);
}
fn line_text(line: &Line<'static>) -> String {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
}
#[test]
fn wraps_warning_emoji_sequence_by_display_width() {
let text = "- ⚠️ Existing worktree changes remain";
let max_width = UnicodeWidthStr::width(text) - 1;
let rows = wrap_spans_to_width(vec![Span::raw(text.to_string())], max_width);
assert_eq!(rows.len(), 2);
assert_eq!(
rows.iter().map(|row| spans_text(row)).collect::<String>(),
text
);
assert!(
rows.iter().all(|row| spans_display_width(row) <= max_width),
"rows exceeded {max_width}: {rows:?}"
);
}
#[test]
fn wide_card_borders_hold_with_grapheme_clusters() {
let state = linked_tool_state(
"call_read",
"read",
"read emoji",
ActivityStatus::Success,
Vec::new(),
"👨👩👧👦 ⌨️ e\u{0301}".repeat(8).as_str(),
);
for line in visual_lines(&state, CARD_LAYOUT_MIN_WIDTH) {
let text = line_text(&line.line);
assert_eq!(
UnicodeWidthStr::width(text.as_str()),
usize::from(CARD_LAYOUT_MIN_WIDTH),
"{text}"
);
if text.starts_with('│') {
assert!(text.ends_with('│'), "{text}");
}
}
}
#[test]
fn bounded_lines_truncates_long_display_width_without_breaking_graphemes() {
let input = format!("{}\nkept", "👨👩👧👦".repeat(140));
let bounded = bounded_lines(&input, 2);
let first = bounded.lines().next().unwrap();
assert!(first.ends_with("… [truncated]"), "{first}");
assert!(UnicodeWidthStr::width(first) <= PREVIEW_LINE_MAX_DISPLAY_WIDTH);
assert!(first.is_char_boundary(first.trim_end_matches("… [truncated]").len()));
assert!(bounded.contains("\nkept"), "{bounded}");
}
#[test]
fn wide_visual_lines_draw_full_borders_and_wrap_body_rows() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu".into(),
});
let rendered = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH)
.into_iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>();
assert!(rendered.first().is_some_and(|line| line.starts_with('┌')));
assert!(rendered.first().is_some_and(|line| line.ends_with('┐')));
assert!(rendered.last().is_some_and(|line| line.starts_with('└')));
assert!(rendered.last().is_some_and(|line| line.ends_with('┘')));
for line in &rendered {
assert_eq!(
UnicodeWidthStr::width(line.as_str()),
usize::from(CARD_LAYOUT_MIN_WIDTH),
"{line}"
);
}
let body_rows = rendered
.iter()
.filter(|line| line.starts_with('│'))
.collect::<Vec<_>>();
assert!(
body_rows.len() > 1,
"body should wrap into bordered rows: {rendered:?}"
);
for row in body_rows {
assert!(row.ends_with('│'), "{row}");
}
}
#[test]
fn assistant_markdown_code_gutter_stays_visual_but_not_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```rust\nfn main() {}\n```".into(),
});
let rendered = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH)
.into_iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("│ fn main() {}"), "{rendered}");
let copy_text = state.transcript_visual_text();
assert!(copy_text.contains("fn main() {}"), "{copy_text}");
assert!(!copy_text.contains("│ fn main() {}"), "{copy_text}");
}
#[test]
fn compact_assistant_markdown_code_gutter_stays_visual_but_not_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let compact_lines = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH - 1);
let rendered = compact_lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ alpha"), "{rendered}");
let copy_text = compact_lines
.iter()
.filter(|line| line.copyable)
.map(|line| line.copy_text.as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(copy_text.contains("alpha"), "{copy_text}");
assert!(!copy_text.contains("│ alpha"), "{copy_text}");
}
#[test]
fn compact_code_gutter_visual_selection_copy_starts_at_visible_code_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH - 1;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains(" │ alpha"), "{rendered}");
let alpha_row = visual_lines(&state, width)
.iter()
.position(|line| line_text(&line.line).contains(" │ alpha"))
.expect("alpha row");
let start = text_position_for_visual_cell(&state, alpha_row, 4, width).expect("alpha start");
let end = text_position_for_visual_cell(&state, alpha_row, 9, width).expect("alpha end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("alpha".to_string())
);
}
#[test]
fn wide_code_gutter_visual_selection_copy_starts_at_visible_code_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "```text\nalpha\n```".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("│ │ alpha"), "{rendered}");
let alpha_row = visual_lines(&state, width)
.iter()
.position(|line| line_text(&line.line).contains("│ │ alpha"))
.expect("alpha row");
let start = text_position_for_visual_cell(&state, alpha_row, 6, width).expect("alpha start");
let end = text_position_for_visual_cell(&state, alpha_row, 11, width).expect("alpha end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("alpha".to_string())
);
}
#[test]
fn wide_assistant_literal_pipe_copy_selection_preserves_authored_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "│ literal".into(),
});
let width = CARD_LAYOUT_MIN_WIDTH;
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("│ │ literal"), "{rendered}");
let start = text_position_for_visual_cell(&state, 1, 4, width).expect("pipe start");
let end = text_position_for_visual_cell(&state, 1, 13, width).expect("literal end");
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(crate::tui::layout::TuiPane::Transcript, width),
Some("│ literal".to_string())
);
}
#[test]
fn wide_visual_text_copy_excludes_full_card_border_glyphs() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "copy text without decoration".into(),
});
let copy_text = visual_text(&state);
assert!(copy_text.contains("👤You"));
assert!(copy_text.contains("copy text without decoration"));
for border in ['┌', '┐', '└', '┘', '│', '─'] {
assert!(
!copy_text.contains(border),
"copy text leaked {border}: {copy_text}"
);
}
}
#[test]
fn compact_visual_lines_at_narrow_widths_have_no_card_border_and_copy_has_no_artifacts() {
for width in [20, 32, CARD_LAYOUT_MIN_WIDTH - 1] {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello compact copy".into(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "plain assistant answer".into(),
});
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert!(!rendered.contains('┌'), "width {width}");
assert!(!rendered.contains('│'), "width {width}");
assert!(!rendered.contains('─'), "width {width}");
let copy_text = visual_text_for_width(&state, width);
let copy_len = copy_text.len();
state.start_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::input::selection::TextPosition::new(0, 0, 0),
);
state.update_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::input::selection::TextPosition::new(copy_len, 0, copy_len),
);
let copied = state
.finish_selection(crate::tui::layout::TuiPane::Transcript, width)
.expect("copied text");
assert!(copied.contains("hello compact copy"));
assert!(copied.contains("plain assistant answer"));
assert!(!copied.contains('┌'));
assert!(!copied.contains('│'));
assert!(!copied.contains('─'));
}
}
#[test]
fn wide_and_compact_visual_lines_branch_at_card_layout_threshold() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello".into(),
});
state
.transcript
.push_back("tool: ⟳ bash • running".to_string());
let compact = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH - 1);
let threshold = visual_lines(&state, CARD_LAYOUT_MIN_WIDTH);
let wide = visual_lines(&state, 80);
assert!(compact.iter().any(|line| line.copy_text == "👤You"));
assert!(threshold.iter().any(|line| line.copy_text == "👤You"));
assert!(
threshold
.iter()
.map(|line| line_text(&line.line))
.any(|text| text.starts_with('┌'))
);
assert!(
wide.iter()
.map(|line| line_text(&line.line))
.any(|text| text.starts_with('┌'))
);
assert!(
compact
.iter()
.map(|line| line_text(&line.line))
.all(|text| !text.starts_with('┌') && !text.starts_with('│'))
);
assert_ne!(
compact
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>(),
threshold
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
);
assert_eq!(
compact
.iter()
.filter(|line| !line.copy_text.is_empty())
.map(|line| line.copy_text.as_str())
.collect::<Vec<_>>(),
wide.iter()
.filter(|line| !line.copy_text.is_empty())
.map(|line| line.copy_text.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn wide_visual_text_position_maps_decorated_body_to_copy_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hello".into(),
});
let start =
text_position_for_visual_cell(&state, 1, 4, CARD_LAYOUT_MIN_WIDTH).expect("body start");
let end = text_position_for_visual_cell(&state, 1, 9, CARD_LAYOUT_MIN_WIDTH).expect("body end");
assert_eq!(
state.transcript_visual_text().get(start.byte..end.byte),
Some("hello")
);
state.start_selection(crate::tui::layout::TuiPane::Transcript, start);
state.update_selection(crate::tui::layout::TuiPane::Transcript, end);
assert_eq!(
state.finish_selection(
crate::tui::layout::TuiPane::Transcript,
CARD_LAYOUT_MIN_WIDTH
),
Some("hello".to_string())
);
}
#[test]
fn activity_preview_updates_invalidate_transcript_card_projection() {
let mut state = MissionControlState::default();
let id = ActivityId::new("call_read");
state
.transcript
.push_back("tool: ⟳ read src/lib.rs • running".to_string());
state.transcript_activity_links.insert(
0,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "read".to_string(),
},
);
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("read src/lib.rs"),
});
let before = state.transcript_visual_text();
assert!(!before.contains("fresh preview"));
state.apply_activity_event(ActivityEvent::Delta {
id,
preview: "fresh preview".to_string(),
});
assert!(state.transcript_visual_text().contains("fresh preview"));
}
#[test]
fn streaming_assistant_renders_markdown_as_plain_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: format!("{}**bold** {}", "plain ".repeat(40), "tail ".repeat(40)),
});
let cards = project_cards(&state);
assert_eq!(cards[0].status, TranscriptCardStatus::Running);
let text = plain_projection(&cards[0].assistant_lines);
assert!(text.contains("**bold**"), "{text}");
}
#[test]
fn completed_assistant_projects_plain_first_then_rich_without_changing_body() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "**bold**".to_string(),
});
state.begin_final_assistant_plain_first_paint();
let first = project_cards(&state);
assert_eq!(first[0].status, TranscriptCardStatus::Success);
assert_eq!(first[0].body, "**bold**");
assert_eq!(plain_projection(&first[0].assistant_lines), "**bold**");
state.finish_final_assistant_plain_first_paint();
let second = project_cards(&state);
assert_eq!(second[0].body, "**bold**");
assert_eq!(plain_projection(&second[0].assistant_lines), "bold");
}
#[test]
fn completed_assistant_plain_first_waits_while_transcript_selection_active() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "**bold**".to_string(),
});
state.start_selection(
crate::tui::layout::TuiPane::Transcript,
crate::tui::input::selection::TextPosition::new(0, 0, 0),
);
assert!(!state.begin_final_assistant_plain_first_paint());
let selected_visible = state.visible_transcript_lines(CARD_LAYOUT_MIN_WIDTH, 10);
let selected_render = selected_visible
.lines
.iter()
.map(line_text)
.collect::<Vec<_>>()
.join("\n");
assert!(selected_render.contains("bold"), "{selected_render}");
assert!(!selected_render.contains("**bold**"), "{selected_render}");
assert!(state.clear_selection());
state.begin_final_assistant_plain_first_paint();
let first = project_cards(&state);
assert_eq!(first[0].body, "**bold**");
assert_eq!(plain_projection(&first[0].assistant_lines), "**bold**");
state.finish_final_assistant_plain_first_paint();
let second = project_cards(&state);
assert_eq!(second[0].body, "**bold**");
assert_eq!(plain_projection(&second[0].assistant_lines), "bold");
}
#[test]
fn completed_assistant_renders_markdown_projection() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "**bold**".to_string(),
});
let cards = project_cards(&state);
assert_eq!(cards[0].status, TranscriptCardStatus::Success);
let text = plain_projection(&cards[0].assistant_lines);
assert_eq!(text, "bold");
}
#[test]
fn tool_preview_card_body_is_line_bounded_without_visible_truncation_marker() {
let marker = "[… activity preview truncated …]";
let body = bounded_lines("one\ntwo\nthree\nfour\nfive", 4);
assert_eq!(body, "one\ntwo\nthree\nfour");
assert_eq!(body.lines().count(), 4);
assert!(!body.contains(marker));
}
#[test]
fn unknown_legacy_prefix_is_diagnostic_not_empty() {
let state = MissionControlState {
transcript: vec!["mystery: value".to_string()].into(),
..Default::default()
};
assert_eq!(roles(&state), vec![TranscriptCardRole::Diagnostic]);
}