use super::*;
#[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, "");
assert_eq!(cards[1].title, "Reasoning");
assert_eq!(cards[2].title, "Agent");
}
#[test]
fn user_prompt_card_has_one_padding_row_above_its_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "prompt".into(),
});
let width = 48;
let lines = visual_lines(&state, width);
let rendered = lines
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>();
assert!(rendered.len() >= 3);
assert!(rendered[0].trim().is_empty());
assert!(rendered[1].contains("User: prompt"));
assert!(rendered[2].trim().is_empty());
}
#[test]
fn groups_adjacent_reasoning_rows_with_boundaries_and_identity_preserved() {
let mut state = MissionControlState::default();
state.transcript.extend([
"thinking: A".to_string(),
"thinking: B".to_string(),
"assistant: answer".to_string(),
"thinking: C".to_string(),
"thinking: C".to_string(),
]);
let cards = project_cards(&state);
assert_eq!(
cards.iter().map(|card| card.role).collect::<Vec<_>>(),
vec![
TranscriptCardRole::Reasoning,
TranscriptCardRole::Assistant,
TranscriptCardRole::Reasoning,
]
);
assert_eq!(cards[0].id.entry_index, 0);
assert_eq!(cards[0].body, "A\n\nB");
assert_eq!(cards[2].body, "C\n\nC");
for width in [CARD_LAYOUT_MIN_WIDTH - 1, CARD_LAYOUT_MIN_WIDTH] {
let rendered = visual_lines(&state, width)
.iter()
.map(|line| line_text(&line.line))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(rendered.matches("Reasoning").count(), 2, "width {width}");
assert!(rendered.contains("A"), "{rendered}");
assert!(rendered.contains("B"), "{rendered}");
}
}
#[test]
fn grouped_tail_scrollbar_estimate_scales_by_consumed_source_rows() {
let mut state = MissionControlState::default();
for index in 0..400 {
state
.transcript
.push_back(format!("session: ordinary entry {index}"));
}
for index in 0..100 {
state
.transcript
.push_back(format!("thinking: reasoning member {index}"));
}
let width = 80;
let exact_rows = visual_lines(&state, width)
.iter()
.map(|line| visual_line_rows(line, width))
.sum::<usize>();
state.invalidate_transcript_cache();
let visible = visible_scrollback_visual_lines(&state, width, 6, 0);
assert!(visible.scrollbar.content_length > 6);
assert!(
visible.scrollbar.content_length <= exact_rows.saturating_mul(2),
"estimate {} should stay proportional to exact rows {exact_rows}",
visible.scrollbar.content_length
);
}
#[test]
fn scrollbar_measurement_becomes_exact_at_start_of_scrollback() {
let mut state = MissionControlState::default();
for index in 0..400 {
state
.transcript
.push_back(format!("session: compact entry {index}"));
}
for index in 0..100 {
state.transcript.push_back(format!(
"assistant: wrapped entry {index} {}",
"wrapped ".repeat(80)
));
}
let width = 80;
let viewport = 6;
let exact_rows = visual_lines(&state, width)
.iter()
.map(|line| visual_line_rows(line, width))
.sum::<usize>();
let exact_overflow = exact_rows.saturating_sub(viewport);
state.invalidate_transcript_cache();
let tail = visible_scrollback_visual_lines(&state, width, viewport as u16, 0);
assert!(
tail.scrollbar.content_length > exact_overflow.saturating_add(1),
"tail-heavy wrapping should begin with an overestimate"
);
let middle =
visible_scrollback_visual_lines(&state, width, viewport as u16, exact_overflow / 2);
assert_ne!(
middle.scrollbar.content_length,
tail.scrollbar.content_length
);
assert!(middle.scrollbar.position > 0);
let top = visible_scrollback_visual_lines(&state, width, viewport as u16, exact_overflow);
assert_eq!(top.scrollbar.position, 0);
assert_eq!(
top.scrollbar.content_length,
exact_overflow.saturating_add(1)
);
let returned_to_tail = visible_scrollback_visual_lines(&state, width, viewport as u16, 0);
assert_eq!(returned_to_tail.scrollbar.position, exact_overflow);
assert_eq!(
returned_to_tail.scrollbar.content_length,
exact_overflow.saturating_add(1)
);
}
#[test]
fn automatic_prompt_uses_legacy_diagnostic_projection_and_full_line_copy() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AutomaticUserPrompt {
text: "continue after compaction".to_string(),
});
let card = &project_cards(&state)[0];
assert_eq!(card.role, TranscriptCardRole::Diagnostic);
assert_eq!(card.title, "Legacy diagnostic");
assert_eq!(card.body, "you [automatic]: continue after compaction");
let copied = visual_text(&state);
assert!(
copied.contains("you [automatic]: continue after compaction"),
"{copied}"
);
assert!(
!copied.contains("User: continue after compaction"),
"{copied}"
);
}