use super::super::*;
use super::helpers::*;
#[test]
fn input_compact_preview_for_image_path() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
let image_path = "/tmp/Screenshot 2026-02-06 at 3.39.48 PM.png";
session.insert_paste_text(image_path);
let data = session.build_input_widget_data(VIEW_WIDTH, VIEW_ROWS);
let rendered = text_content(&data.text);
assert!(rendered.contains("[Image:"));
assert!(rendered.contains("Screenshot 2026-02-06"));
}
#[test]
fn input_compact_preview_for_quoted_image_path() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
let image_path = "\"/tmp/Screenshot 2026-02-06 at 3.39.48 PM.png\"";
session.insert_paste_text(image_path);
let data = session.build_input_widget_data(VIEW_WIDTH, VIEW_ROWS);
let rendered = text_content(&data.text);
assert!(rendered.contains("[Image:"));
assert!(rendered.contains("Screenshot 2026-02-06"));
}
#[test]
fn control_e_launches_editor() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
let event = KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL);
let result = session.process_key(event);
assert!(matches!(result, Some(InlineEvent::LaunchEditor)));
}
#[test]
fn control_e_moves_cursor_to_end_when_input_has_content() {
let text = "hello world";
let mut session = session_with_input(text, 0);
let result = session.process_key(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(result.is_none());
assert_eq!(session.cursor(), text.len());
}
#[test]
fn control_g_launches_editor_from_plan_confirmation_modal() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.input_status_right = Some("model | 25% context".to_string());
let plan = app_types::PlanContent::from_markdown(
"Test Plan".to_string(),
"## Plan of Work\n- Step 1",
Some(".vtcode/plans/test-plan.md".to_string()),
);
show_plan_confirmation_overlay(&mut session, plan);
let event = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL);
let result = session.process_key(event);
assert!(matches!(
result,
Some(InlineEvent::Overlay(OverlayEvent::Submitted(
OverlaySubmission::Hotkey(OverlayHotkeyAction::LaunchEditor)
)))
));
assert!(session.modal_state().is_none());
}
#[test]
fn plan_confirmation_modal_matches_four_way_gate_copy() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
let plan = app_types::PlanContent::from_markdown(
"Test Plan".to_string(),
"## Implementation Plan\n1. Step",
Some(".vtcode/plans/test-plan.md".to_string()),
);
show_plan_confirmation_overlay(&mut session, plan);
let modal = session
.modal_state()
.expect("plan confirmation modal should be present");
assert_eq!(modal.title, "Ready to code?");
assert_eq!(
modal.lines.first().map(String::as_str),
Some("A plan is ready to execute. Would you like to proceed?")
);
let list = modal
.list
.as_ref()
.expect("plan confirmation should include list options");
assert_eq!(list.items.len(), 3);
assert_eq!(list.items[0].title, "Yes, auto-accept edits");
assert_eq!(
list.items[0].subtitle.as_deref(),
Some("Execute with auto-approval.")
);
assert_eq!(list.items[0].badge.as_deref(), Some("Recommended"));
assert_eq!(list.items[1].title, "Yes, manually approve edits");
assert_eq!(
list.items[1].subtitle.as_deref(),
Some("Keep context and confirm each edit before applying.")
);
assert_eq!(list.items[2].title, "Type feedback to revise the plan");
assert_eq!(
list.items[2].subtitle.as_deref(),
Some("Return to plan mode and refine the plan.")
);
}
#[test]
fn arrow_keys_never_launch_editor() {
let text = "hello world";
let mut session = session_with_input(text, 0);
for modifiers in [
KeyModifiers::empty(),
KeyModifiers::CONTROL,
KeyModifiers::SHIFT,
KeyModifiers::ALT,
KeyModifiers::SUPER,
KeyModifiers::META,
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
KeyModifiers::CONTROL | KeyModifiers::SUPER,
] {
let event = KeyEvent::new(KeyCode::Right, modifiers);
let result = session.process_key(event);
assert!(
!matches!(result, Some(InlineEvent::LaunchEditor)),
"Right arrow with modifiers {:?} should not launch editor",
modifiers
);
}
for key_code in [KeyCode::Left, KeyCode::Up, KeyCode::Down] {
let event = KeyEvent::new(key_code, KeyModifiers::SUPER);
let result = session.process_key(event);
assert!(
!matches!(result, Some(InlineEvent::LaunchEditor)),
"{:?} with SUPER should not launch editor",
key_code
);
}
}
#[test]
fn timeline_hidden_keeps_navigation_unselected() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.push_line(InlineMessageKind::Agent, vec![make_segment("Response")]);
let backend = TestBackend::new(VIEW_WIDTH, VIEW_ROWS);
let mut terminal = Terminal::new(backend).expect("failed to create test terminal");
terminal
.draw(|frame| session.render(frame))
.expect("failed to render session with hidden timeline");
assert!(session.navigation_state.selected().is_none());
}
#[test]
fn active_file_operation_indicator_renders_spinner_frame() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.push_line(
InlineMessageKind::Info,
vec![make_segment("❋ Editing vtcode.toml...")],
);
session.handle_command(InlineCommand::SetInputStatus {
left: Some("Running tool: edit_file".to_string()),
right: None,
});
let rendered = rendered_transcript_widget_lines(&mut session, VIEW_WIDTH, VIEW_ROWS);
assert!(
rendered
.iter()
.any(|line| line.contains("⠋ Editing vtcode.toml...")),
"active file operation indicator should show a spinner frame"
);
assert!(
!rendered
.iter()
.any(|line| line.contains("❋ Editing vtcode.toml...")),
"spinner should replace the static file operation marker while active"
);
}
#[test]
fn non_file_tool_status_keeps_static_file_operation_indicator() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.push_line(
InlineMessageKind::Info,
vec![make_segment("❋ Editing vtcode.toml...")],
);
session.handle_command(InlineCommand::SetInputStatus {
left: Some("Running tool: unified_search".to_string()),
right: None,
});
let rendered = rendered_transcript_widget_lines(&mut session, VIEW_WIDTH, VIEW_ROWS);
assert!(
rendered
.iter()
.any(|line| line.contains("❋ Editing vtcode.toml...")),
"non-file tool activity should not animate stale file operation indicators"
);
}
#[test]
fn empty_enter_with_active_pty_opens_jobs() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.active_pty_sessions = Some(Arc::new(AtomicUsize::new(1)));
let event = session.process_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(event, Some(InlineEvent::Submit(ref value)) if value == "/jobs"));
}
#[test]
fn task_panel_visibility_is_independent_from_logs() {
let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
session.set_task_panel_visible(true);
let initial_task_panel = session.show_task_panel;
let initial_logs = session.core.show_logs;
session.core.toggle_logs();
assert_eq!(session.show_task_panel, initial_task_panel);
assert_ne!(session.core.show_logs, initial_logs);
}
#[test]
fn timeline_visible_selects_latest_item() {
let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
session.push_line(InlineMessageKind::Agent, vec![make_segment("First")]);
session.push_line(InlineMessageKind::Agent, vec![make_segment("Second")]);
let backend = TestBackend::new(VIEW_WIDTH, VIEW_ROWS);
let mut terminal = Terminal::new(backend).expect("failed to create test terminal");
terminal
.draw(|frame| session.render(frame))
.expect("failed to render session with timeline");
assert!(session.navigation_state.selected().is_none());
}
#[test]
fn tool_detail_renders_with_border_and_body_style() {
let theme = themed_inline_colors();
let mut session = Session::new(theme, None, VIEW_ROWS);
let detail_style = InlineTextStyle::default().italic();
session.push_line(
InlineMessageKind::Tool,
vec![InlineSegment {
text: " result line".to_string(),
style: Arc::new(detail_style),
}],
);
let index = session
.lines
.len()
.checked_sub(1)
.expect("tool detail line should exist");
let spans = session.render_message_spans(index);
assert_eq!(spans.len(), 1);
let body_span = &spans[0];
assert!(body_span.style.add_modifier.contains(Modifier::ITALIC));
assert_eq!(body_span.content.clone().into_owned(), " result line");
}
#[test]
fn top_level_task_tree_tail_line_is_dimmed_in_tool_blocks() {
let theme = themed_inline_colors();
let mut session = Session::new(theme, None, VIEW_ROWS);
session.push_line(
InlineMessageKind::Tool,
vec![InlineSegment {
text: "└ Report actions taken, blockers, and required user input".to_string(),
style: Arc::new(InlineTextStyle::default()),
}],
);
let index = session
.lines
.len()
.checked_sub(1)
.expect("tool detail line should exist");
let transcript_lines = session.reflow_message_lines(index, 100);
let task_span = transcript_lines
.iter()
.flat_map(|line| line.line.spans.iter())
.find(|span| span.content.contains("Report actions taken"))
.expect("expected task span");
assert!(
task_span.style.add_modifier.contains(Modifier::DIM),
"top-level task rows should render dimmed"
);
}