use super::*;
use crate::tui::{render, state::MissionControlState, transcript_projection};
use ratatui::{Terminal, backend::TestBackend};
fn image_state() -> MissionControlState {
let mut state = MissionControlState::default();
let mut images = TranscriptImages::start((10, 20));
images.cache.push_back((
"image.png".into(),
Some(Arc::new(PreparedImage {
width: 400,
height: 400,
encoded: String::new(),
})),
));
state.images = std::cell::RefCell::new(images);
state
.transcript
.push_back("assistant: ");
state
}
fn draw(state: &MissionControlState, width: u16, height: u16) {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal.draw(|frame| render::draw(frame, state)).unwrap();
}
#[test]
fn markdown_images_have_scroll_rows_but_no_copyable_graphics_codes() {
let state = image_state();
let lines = transcript_projection::uncached_transcript_visual_lines(&state, 60);
let rows: Vec<_> = lines
.iter()
.filter_map(|line| line.image_row.as_ref())
.collect();
assert_eq!(rows.len(), usize::from(PREVIEW_ROWS));
assert_eq!(
rows.iter().map(|row| row.row).collect::<Vec<_>>(),
(0..PREVIEW_ROWS).collect::<Vec<_>>()
);
assert!(
lines
.iter()
.filter(|line| line.image_row.is_some())
.all(|line| !line.copyable && line.copy_text.is_empty())
);
assert!(lines.iter().all(|line| !line.copy_text.contains('\x1b')));
}
#[test]
fn native_placements_stay_inside_transcript_and_disappear_under_overlays() {
let mut state = image_state();
draw(&state, 120, 40);
let area = render::transcript_text_area_for_pane(
render::layout_for(Rect::new(0, 0, 120, 40), &state).transcript,
);
assert_eq!(
state.images.borrow().placements.len(),
usize::from(PREVIEW_ROWS)
);
assert!(
state
.images
.borrow()
.placements
.iter()
.all(|p| p.area.intersection(area) == p.area)
);
state.show_copy_success_toast(std::time::Instant::now());
draw(&state, 120, 40);
assert!(state.images.borrow().placements.is_empty());
assert!(state.images.borrow().visible_paths.is_empty());
state.toast = None;
draw(&state, 120, 40);
assert_eq!(
state.images.borrow().placements.len(),
usize::from(PREVIEW_ROWS)
);
state.transcript.clear();
draw(&state, 120, 40);
assert!(state.images.borrow().placements.is_empty());
}
#[test]
fn short_viewport_crops_image_top_and_preserves_source_row_offsets() {
let state = image_state();
draw(&state, 120, 12);
let images = state.images.borrow();
assert!(!images.placements.is_empty());
assert!(images.placements.len() < usize::from(PREVIEW_ROWS));
assert!(images.placements[0].source_y > 0);
assert_eq!(
images.placements.last().unwrap().source_y
+ images.placements.last().unwrap().source_height,
400
);
}
#[test]
fn unsupported_terminals_and_remote_markdown_keep_text_only() {
let mut state = MissionControlState::default();
state
.transcript
.push_back("assistant: ");
let lines = transcript_projection::uncached_transcript_visual_lines(&state, 60);
assert!(lines.iter().all(|line| line.image_row.is_none()));
assert!(
lines
.iter()
.any(|line| line.copy_text.contains("sample image"))
);
let mut state = image_state();
state.transcript.clear();
state.transcript.push_back(
"assistant:  ",
);
let lines = transcript_projection::uncached_transcript_visual_lines(&state, 60);
assert!(lines.iter().all(|line| line.image_row.is_none()));
}
#[test]
fn view_image_uses_parameter_reference_not_display_metadata_as_authority() {
use crate::output::{ActivityEvent, ActivityId, ActivityStatus, ToolActivityDetail};
let mut state = image_state();
state.transcript.clear();
state.transcript.push_back("tool: ✓ view_image • done");
let id = ActivityId::new("view");
state.transcript.link_activity(
0,
crate::tui::state::TranscriptActivityLink {
activity_id: id.clone(),
tool_name: "view_image".into(),
},
);
state.apply_activity_event(ActivityEvent::ToolResultDetail {
id,
detail: ToolActivityDetail {
tool_name: "view_image".into(),
label: "image".into(),
params: serde_json::json!({"path":"image.png"}),
metadata: serde_json::json!({"path":"wrong.png"}),
status: ActivityStatus::Success,
output: "A sample".into(),
applied_diff: None,
},
});
let lines = transcript_projection::uncached_transcript_visual_lines(&state, 60);
let rows: Vec<_> = lines
.iter()
.filter_map(|line| line.image_row.as_ref())
.collect();
assert_eq!(rows.len(), usize::from(PREVIEW_ROWS));
assert!(rows.iter().all(|row| row.path == "image.png"));
let preview_lines: Vec<_> = lines
.iter()
.filter(|line| line.image_row.is_some())
.collect();
let mut buffer = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 60, PREVIEW_ROWS));
use ratatui::widgets::Widget;
ratatui::widgets::Paragraph::new(
preview_lines
.iter()
.map(|line| line.line.clone())
.collect::<Vec<_>>(),
)
.render(buffer.area, &mut buffer);
for cell in &buffer.content {
assert_eq!(cell.symbol(), " ", "image pixels must not overlap text");
assert_eq!(cell.bg, state.theme.surface_panel());
}
}