//! Coverage for history-cell rendering, wrapping, and transcript behavior.use super::*;use crate::exec_cell::CommandOutput;use crate::exec_cell::ExecCall;use crate::exec_cell::ExecCell;use crate::legacy_core::config::Config;use crate::legacy_core::config::ConfigBuilder;use crate::session_state::ThreadSessionState;use crate::wrapping::word_wrap_lines;use codex_app_server_protocol::AskForApproval;use codex_app_server_protocol::McpAuthStatus;use codex_config::types::McpServerConfig;use codex_otel::RuntimeMetricTotals;use codex_otel::RuntimeMetricsSummary;use codex_protocol::ThreadId;use codex_protocol::account::PlanType;use codex_protocol::parse_command::ParsedCommand;use dirs::home_dir;use pretty_assertions::assert_eq;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use serde_json::json;use std::collections::HashMap;use std::path::PathBuf;use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;use codex_protocol::mcp::CallToolResult;use codex_protocol::mcp::Tool;use rmcp::model::Content;const SMALL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";async fn test_config() -> Config { let codex_home = std::env::temp_dir(); ConfigBuilder::default() .codex_home(codex_home.clone()) .build() .await .expect("config")}fn test_cwd() -> PathBuf { // These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or // Windows-specific root semantics into the fixtures. std::env::temp_dir()}#[test]fn streaming_agent_tail_blank_line_uses_one_viewport_row() { let cell = StreamingAgentTailCell::new( vec![ HyperlinkLine::from("first"), HyperlinkLine::from(""), HyperlinkLine::from("second"), ], /*is_first_line*/ false, ); let lines = cell.display_lines(/*width*/ 80); insta::assert_snapshot!(render_lines(&lines).join("\n"), @" first second"); assert_eq!(cell.desired_height(/*width*/ 80), 3);}fn stdio_server_config( command: &str, args: Vec<&str>, env: Option<HashMap<String, String>>, env_vars: Vec<&str>,) -> McpServerConfig { let mut table = toml::Table::new(); table.insert( "command".to_string(), toml::Value::String(command.to_string()), ); if !args.is_empty() { table.insert( "args".to_string(), toml::Value::Array( args.into_iter() .map(|arg| toml::Value::String(arg.to_string())) .collect(), ), ); } if let Some(env) = env { table.insert("env".to_string(), string_map_to_toml_value(env)); } if !env_vars.is_empty() { table.insert( "env_vars".to_string(), toml::Value::Array( env_vars .into_iter() .map(|name| toml::Value::String(name.to_string())) .collect(), ), ); } toml::Value::Table(table) .try_into() .expect("test stdio MCP config should deserialize")}fn streamable_http_server_config( url: &str, bearer_token_env_var: Option<&str>, http_headers: Option<HashMap<String, String>>, env_http_headers: Option<HashMap<String, String>>,) -> McpServerConfig { let mut table = toml::Table::new(); table.insert("url".to_string(), toml::Value::String(url.to_string())); if let Some(bearer_token_env_var) = bearer_token_env_var { table.insert( "bearer_token_env_var".to_string(), toml::Value::String(bearer_token_env_var.to_string()), ); } if let Some(http_headers) = http_headers { table.insert( "http_headers".to_string(), string_map_to_toml_value(http_headers), ); } if let Some(env_http_headers) = env_http_headers { table.insert( "env_http_headers".to_string(), string_map_to_toml_value(env_http_headers), ); } toml::Value::Table(table) .try_into() .expect("test streamable_http MCP config should deserialize")}fn string_map_to_toml_value(entries: HashMap<String, String>) -> toml::Value { toml::Value::Table( entries .into_iter() .map(|(key, value)| (key, toml::Value::String(value))) .collect(), )}fn render_lines(lines: &[Line<'static>]) -> Vec<String> { lines .iter() .map(|line| { line.spans .iter() .map(|span| span.content.as_ref()) .collect::<String>() }) .collect()}fn render_transcript(cell: &dyn HistoryCell) -> Vec<String> { render_lines(&cell.transcript_lines(u16::MAX))}fn assert_unstyled_lines(lines: &[Line<'static>]) { for line in lines { assert_eq!(line.style, Style::default()); for span in &line.spans { assert_eq!(span.style, Style::default()); } }}fn image_block(data: &str) -> serde_json::Value { serde_json::to_value(Content::image(data.to_string(), "image/png")) .expect("image content should serialize")}fn text_block(text: &str) -> serde_json::Value { serde_json::to_value(Content::text(text)).expect("text content should serialize")}fn resource_link_block( uri: &str, name: &str, title: Option<&str>, description: Option<&str>,) -> serde_json::Value { serde_json::to_value(Content::resource_link(rmcp::model::RawResource { uri: uri.to_string(), name: name.to_string(), title: title.map(str::to_string), description: description.map(str::to_string), mime_type: None, size: None, icons: None, meta: None, })) .expect("resource link content should serialize")}#[test]fn raw_lines_from_source_preserves_explicit_blank_lines() { let lines = raw_lines_from_source("alpha\n\nbeta\n"); assert_eq!( render_lines(&lines), vec!["alpha".to_string(), String::new(), "beta".to_string()] ); assert_unstyled_lines(&lines);}#[test]fn raw_lines_from_source_preserves_trailing_blank_but_not_trailing_newline() { assert_eq!( render_lines(&raw_lines_from_source("alpha\n\n")), vec!["alpha".to_string(), String::new()] ); assert_eq!(raw_lines_from_source(""), Vec::<Line<'static>>::new());}#[test]fn source_backed_cells_render_raw_source_without_prefix_or_style() { let user = new_user_prompt( "hello\n\nworld\n".to_string(), Vec::new(), Vec::new(), Vec::new(), ); let assistant = AgentMarkdownCell::new( "- item\n\n| A | B |\n| - | - |\n| x | y |\n".to_string(), &test_cwd(), ); let reasoning = ReasoningSummaryCell::new( "thinking".to_string(), "first thought\n\nsecond thought".to_string(), &test_cwd(), /*transcript_only*/ false, ); let plan = new_proposed_plan( "1. Inspect\n\n```sh\ncargo test\n```".to_string(), &test_cwd(), ); let user_lines = user.raw_lines(); assert_eq!( render_lines(&user_lines), vec!["hello".to_string(), String::new(), "world".to_string()] ); assert_unstyled_lines(&user_lines); let assistant_lines = assistant.raw_lines(); assert_eq!( render_lines(&assistant_lines), vec![ "- item".to_string(), String::new(), "| A | B |".to_string(), "| - | - |".to_string(), "| x | y |".to_string(), ] ); assert_unstyled_lines(&assistant_lines); let reasoning_lines = reasoning.raw_lines(); assert_eq!( render_lines(&reasoning_lines), vec![ "first thought".to_string(), String::new(), "second thought".to_string(), ] ); assert_unstyled_lines(&reasoning_lines); let plan_lines = plan.raw_lines(); assert_eq!( render_lines(&plan_lines), vec![ "1. Inspect".to_string(), String::new(), "```sh".to_string(), "cargo test".to_string(), "```".to_string(), ] ); assert_unstyled_lines(&plan_lines);}#[test]fn proposed_plan_cell_renders_markdown_table() { let plan = new_proposed_plan( "## Plan\n\n| Step | Owner |\n| --- | --- |\n| Verify | Codex |\n".to_string(), &test_cwd(), ); let rendered = render_lines(&plan.display_lines(/*width*/ 80)); assert!( rendered.iter().any(|line| line.contains('━')), "expected separated table in proposed plan output: {rendered:?}" ); assert!( !rendered .iter() .any(|line| line.trim() == "| Step | Owner |"), "did not expect raw table header in rich proposed plan output: {rendered:?}" ); let raw = render_lines(&plan.raw_lines()); assert!( raw.iter().any(|line| line == "| Step | Owner |"), "expected raw mode to preserve table markdown source: {raw:?}" );}#[test]fn proposed_plan_cell_preserves_wrapped_table_web_links() { let destination = "https://example.com/a/very/long/path/to/a/table/artifact"; let plan = new_proposed_plan( format!("| Step | URL |\n| --- | --- |\n| Verify | {destination} |\n"), &test_cwd(), ); let lines = plan.display_hyperlink_lines(/*width*/ 32); let linked_rows = lines .iter() .filter(|line| !line.hyperlinks.is_empty()) .collect::<Vec<_>>(); assert!(linked_rows.len() > 1); assert!(linked_rows.iter().all(|line| { line.hyperlinks .iter() .all(|link| link.destination == destination) }));}#[test]fn composite_cell_preserves_child_web_links() { let destination = "https://chatgpt.com/codex/settings/usage"; let cell = CompositeHistoryCell::new(vec![ Box::new(PlainHistoryCell::new(vec![Line::from("/status")])), Box::new(WebHyperlinkHistoryCell::new(vec![Line::from(destination)])), ]); let lines = cell.display_hyperlink_lines(/*width*/ 80); assert_eq!( lines[2].hyperlinks, vec![crate::terminal_hyperlinks::TerminalHyperlink { columns: 0..destination.len(), destination: destination.to_string(), }] );}#[test]fn proposed_plan_cell_unwraps_markdown_fenced_table() { let plan = new_proposed_plan( "## Plan\n\n```markdown\n| Step | Owner |\n| --- | --- |\n| Verify | Codex |\n```\n" .to_string(), &test_cwd(), ); let rendered = render_lines(&plan.display_lines(/*width*/ 80)); assert!( rendered.iter().any(|line| line.contains('━')), "expected separated table for markdown-fenced proposed plan output: {rendered:?}" ); assert!( !rendered.iter().any(|line| line.trim() == "```markdown"), "did not expect markdown fence to render as code block: {rendered:?}" );}#[test]fn structured_tool_cell_renders_raw_plain_text_without_prefix_or_style() { let invocation = McpInvocation { server: "search".into(), tool: "find_docs".into(), arguments: Some(json!({"query": "raw mode"})), }; let result = CallToolResult { content: vec![text_block("alpha\nbeta")], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-raw".to_string(), invocation, /*animations_enabled*/ false, ); assert!( cell.complete(Duration::from_millis(1), Ok(result)) .is_none() ); let lines = cell.raw_lines(); let rendered = render_lines(&lines); assert!(rendered[0].starts_with("Called search.find_docs(")); assert_eq!(rendered[1..], ["alpha".to_string(), "beta".to_string()]); assert_unstyled_lines(&lines);}#[test]fn raw_mode_toggle_transcript_snapshot() { let mut tool_cell = new_active_mcp_tool_call( "call-snapshot".to_string(), McpInvocation { server: "workspace".to_string(), tool: "inspect".to_string(), arguments: Some(json!({"path": "README.md"})), }, /*animations_enabled*/ false, ); assert!( tool_cell .complete( Duration::from_millis(5), Ok(CallToolResult { content: vec![text_block("structured output\nsecond line")], is_error: None, structured_content: None, meta: None, }), ) .is_none() ); let cells: Vec<Box<dyn HistoryCell>> = vec![ Box::new(new_user_prompt( "Please format this\nfor copying".to_string(), Vec::new(), Vec::new(), Vec::new(), )), Box::new(AgentMarkdownCell::new( "- first item\n- second item\n\n| Col | Value |\n| --- | --- |\n| code | `x = 1` |\n\n```text\ncopy me\n```".to_string(), &test_cwd(), )), Box::new(tool_cell), ]; let render = |mode| { cells .iter() .flat_map(|cell| cell.display_lines_for_mode(/*width*/ 40, mode)) .map(|line| { line.spans .into_iter() .map(|span| span.content.into_owned()) .collect::<String>() }) .collect::<Vec<_>>() .join("\n") }; let rendered = format!( "rich before:\n{}\n\nraw on:\n{}\n\nrich after:\n{}", render(HistoryRenderMode::Rich), render(HistoryRenderMode::Raw), render(HistoryRenderMode::Rich) ); insta::assert_snapshot!("raw_mode_toggle_transcript", rendered);}#[test]fn image_generation_call_renders_saved_path() { let saved_path = test_path_buf("/tmp/generated-image.png").abs(); let expected_saved_path = format!( " └ Saved to: {}", Url::from_file_path(saved_path.as_path()).expect("test path should convert to file URL") ); let cell = new_image_generation_call( "call-image-generation".to_string(), "completed", Some("A tiny blue square".to_string()), Some(saved_path), ); assert_eq!( render_lines(&cell.display_lines(/*width*/ 80)), vec![ "• Generated Image:".to_string(), " └ A tiny blue square".to_string(), expected_saved_path, ], );}fn session_configured_event(model: &str) -> ThreadSessionState { ThreadSessionState { thread_id: ThreadId::new(), forked_from_id: None, fork_parent_title: None, thread_name: None, model: model.to_string(), model_provider_id: "test-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, permission_profile: PermissionProfile::read_only(), active_permission_profile: None, cwd: test_path_buf("/tmp/project").abs(), runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), reasoning_effort: None, collaboration_mode: None, personality: None, message_history: None, network_proxy: None, rollout_path: Some(PathBuf::new()), }}#[test]fn unified_exec_interaction_cell_renders_input() { let cell = new_unified_exec_interaction(Some("echo hello".to_string()), "ls\npwd".to_string()); let lines = render_transcript(&cell); assert_eq!( lines, vec![ "↳ Interacted with background terminal · echo hello", " └ ls", " pwd", ], );}#[test]fn unified_exec_interaction_cell_renders_wait() { let cell = new_unified_exec_interaction(/*command_display*/ None, String::new()); let lines = render_transcript(&cell); assert_eq!(lines, vec!["• Waited for background terminal"]);}#[test]fn final_message_separator_hides_short_worked_label_and_includes_runtime_metrics() { let summary = RuntimeMetricsSummary { tool_calls: RuntimeMetricTotals { count: 3, duration_ms: 2_450, }, api_calls: RuntimeMetricTotals { count: 2, duration_ms: 1_200, }, streaming_events: RuntimeMetricTotals { count: 6, duration_ms: 900, }, websocket_calls: RuntimeMetricTotals { count: 1, duration_ms: 700, }, websocket_events: RuntimeMetricTotals { count: 4, duration_ms: 1_200, }, responses_api_overhead_ms: 650, responses_api_inference_time_ms: 1_940, responses_api_engine_iapi_ttft_ms: 410, responses_api_engine_service_ttft_ms: 460, responses_api_engine_iapi_tbt_ms: 1_180, responses_api_engine_service_tbt_ms: 1_240, turn_ttft_ms: 0, turn_ttfm_ms: 0, }; let cell = FinalMessageSeparator::new(Some(12), Some(summary)); let rendered = render_lines(&cell.display_lines(/*width*/ 600)); assert_eq!(rendered.len(), 1); assert!(!rendered[0].contains("Worked for")); assert!(rendered[0].contains("Local tools: 3 calls (2.5s)")); assert!(rendered[0].contains("Inference: 2 calls (1.2s)")); assert!(rendered[0].contains("WebSocket: 1 events send (700ms)")); assert!(rendered[0].contains("Streams: 6 events (900ms)")); assert!(rendered[0].contains("4 events received (1.2s)")); assert!(rendered[0].contains("Responses API overhead: 650ms")); assert!(rendered[0].contains("Responses API inference: 1.9s")); assert!(rendered[0].contains("TTFT: 410ms (iapi) 460ms (service)")); assert!(rendered[0].contains("TBT: 1.2s (iapi) 1.2s (service)"));}#[test]fn final_message_separator_includes_worked_label_after_one_minute() { let cell = FinalMessageSeparator::new(Some(61), /*runtime_metrics*/ None); let rendered = render_lines(&cell.display_lines(/*width*/ 200)); assert_eq!(rendered.len(), 1); assert!(rendered[0].contains("Worked for"));}#[test]fn ps_output_empty_snapshot() { let cell = new_unified_exec_processes_output(Vec::new()); let rendered = render_lines(&cell.display_lines(/*width*/ 60)).join("\n"); insta::assert_snapshot!(rendered);}#[tokio::test]async fn session_info_uses_availability_nux_tooltip_override() { let config = test_config().await; let cell = new_session_info( &config, "gpt-5", &session_configured_event("gpt-5"), /*is_first_event*/ false, Some("Model just became available".to_string()), Some(PlanType::Free), /*show_fast_status*/ false, ); let rendered = render_transcript(&cell).join("\n"); assert!(rendered.contains("Model just became available"));}#[tokio::test]#[cfg_attr( target_os = "windows", ignore = "snapshot path rendering differs on Windows")]async fn session_info_availability_nux_tooltip_snapshot() { let mut config = test_config().await; config.cwd = test_path_buf("/tmp/project").abs(); let cell = new_session_info( &config, "gpt-5", &session_configured_event("gpt-5"), /*is_first_event*/ false, Some("Model just became available".to_string()), Some(PlanType::Free), /*show_fast_status*/ false, ); let rendered = render_transcript(&cell).join("\n"); insta::assert_snapshot!(rendered);}#[tokio::test]async fn session_info_first_event_suppresses_tooltips_and_nux() { let config = test_config().await; let cell = new_session_info( &config, "gpt-5", &session_configured_event("gpt-5"), /*is_first_event*/ true, Some("Model just became available".to_string()), Some(PlanType::Free), /*show_fast_status*/ false, ); let rendered = render_transcript(&cell).join("\n"); assert!(!rendered.contains("Model just became available")); assert!(rendered.contains("To get started"));}#[tokio::test]async fn session_info_hides_tooltips_when_disabled() { let mut config = test_config().await; config.show_tooltips = false; let cell = new_session_info( &config, "gpt-5", &session_configured_event("gpt-5"), /*is_first_event*/ false, Some("Model just became available".to_string()), Some(PlanType::Free), /*show_fast_status*/ false, ); let rendered = render_transcript(&cell).join("\n"); assert!(!rendered.contains("Model just became available"));}#[test]fn ps_output_multiline_snapshot() { let cell = new_unified_exec_processes_output(vec![ UnifiedExecProcessDetails { command_display: "echo hello\nand then some extra text".to_string(), recent_chunks: vec!["hello".to_string(), "done".to_string()], }, UnifiedExecProcessDetails { command_display: "rg \"foo\" src".to_string(), recent_chunks: vec!["src/main.rs:12:foo".to_string()], }, ]); let rendered = render_lines(&cell.display_lines(/*width*/ 40)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn cyber_policy_error_event_snapshot() { let cell = new_cyber_policy_error_event(); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn cyber_policy_error_event_narrow_snapshot() { let cell = new_cyber_policy_error_event(); let rendered = render_lines(&cell.display_lines(/*width*/ 36)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn ps_output_long_command_snapshot() { let cell = new_unified_exec_processes_output(vec![UnifiedExecProcessDetails { command_display: String::from( "rg \"foo\" src --glob '**/*.rs' --max-count 1000 --no-ignore --hidden --follow --glob '!target/**'", ), recent_chunks: vec!["searching...".to_string()], }]); let rendered = render_lines(&cell.display_lines(/*width*/ 36)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn ps_output_many_sessions_snapshot() { let cell = new_unified_exec_processes_output( (0..20) .map(|idx| UnifiedExecProcessDetails { command_display: format!("command {idx}"), recent_chunks: Vec::new(), }) .collect(), ); let rendered = render_lines(&cell.display_lines(/*width*/ 32)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn ps_output_chunk_leading_whitespace_snapshot() { let cell = new_unified_exec_processes_output(vec![UnifiedExecProcessDetails { command_display: "just fix".to_string(), recent_chunks: vec![ " indented first".to_string(), " more indented".to_string(), ], }]); let rendered = render_lines(&cell.display_lines(/*width*/ 60)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn error_event_oversized_input_snapshot() { let cell = new_error_event( "Message exceeds the maximum length of 1048576 characters (1048577 provided).".to_string(), ); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[tokio::test]async fn mcp_tools_output_masks_sensitive_values() { let mut config = test_config().await; let mut env = HashMap::new(); env.insert("TOKEN".to_string(), "secret".to_string()); let stdio_config = stdio_server_config("docs-server", vec![], Some(env), vec!["APP_TOKEN"]); let mut servers = config.mcp_servers.get().clone(); servers.insert("docs".to_string(), stdio_config); let mut headers = HashMap::new(); headers.insert("Authorization".to_string(), "Bearer secret".to_string()); let mut env_headers = HashMap::new(); env_headers.insert("X-API-Key".to_string(), "API_KEY_ENV".to_string()); let http_config = streamable_http_server_config( "https://example.com/mcp", Some("MCP_TOKEN"), Some(headers), Some(env_headers), ); servers.insert("http".to_string(), http_config); config .mcp_servers .set(servers) .expect("test mcp servers should accept any configuration"); let mut tools: HashMap<String, Tool> = HashMap::new(); tools.insert( "mcp__docs__list".to_string(), Tool { description: None, name: "list".to_string(), title: None, input_schema: serde_json::json!({"type": "object", "properties": {}}), output_schema: None, annotations: None, icons: None, meta: None, }, ); tools.insert( "mcp__http__ping".to_string(), Tool { description: None, name: "ping".to_string(), title: None, input_schema: serde_json::json!({"type": "object", "properties": {}}), output_schema: None, annotations: None, icons: None, meta: None, }, ); let auth_statuses: HashMap<String, McpAuthStatus> = HashMap::new(); let cell = new_mcp_tools_output( &config, tools, HashMap::new(), HashMap::new(), &auth_statuses, ); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[tokio::test]async fn mcp_tools_output_lists_tools_for_hyphenated_server_names() { let mut config = test_config().await; let mut servers = config.mcp_servers.get().clone(); servers.insert( "some-server".to_string(), stdio_server_config("docs-server", vec!["--stdio"], /*env*/ None, vec![]), ); config .mcp_servers .set(servers) .expect("test mcp servers should accept any configuration"); let tools = HashMap::from([( "mcp__some_server__lookup".to_string(), Tool { description: None, name: "lookup".to_string(), title: None, input_schema: serde_json::json!({"type": "object", "properties": {}}), output_schema: None, annotations: None, icons: None, meta: None, }, )]); let auth_statuses: HashMap<String, McpAuthStatus> = HashMap::new(); let cell = new_mcp_tools_output( &config, tools, HashMap::new(), HashMap::new(), &auth_statuses, ); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn mcp_tools_output_from_statuses_renders_status_only_servers() { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), server_info: None, tools: HashMap::from([( "lookup".to_string(), Tool { description: None, name: "lookup".to_string(), title: None, input_schema: serde_json::json!({"type": "object", "properties": {}}), output_schema: None, annotations: None, icons: None, meta: None, }, )]), resources: Vec::new(), resource_templates: Vec::new(), auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported, }]; let cell = new_mcp_tools_output_from_statuses(&statuses, McpServerStatusDetail::ToolsAndAuthOnly); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn mcp_tools_output_from_statuses_renders_verbose_inventory() { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), server_info: None, tools: HashMap::from([( "lookup".to_string(), Tool { description: None, name: "lookup".to_string(), title: None, input_schema: serde_json::json!({"type": "object", "properties": {}}), output_schema: None, annotations: None, icons: None, meta: None, }, )]), resources: vec![Resource { annotations: None, description: None, mime_type: None, name: "docs".to_string(), size: None, title: Some("Docs".to_string()), uri: "file:///docs".to_string(), icons: None, meta: None, }], resource_templates: vec![ResourceTemplate { annotations: None, uri_template: "file:///docs/{id}".to_string(), name: "doc-template".to_string(), title: Some("Doc Template".to_string()), description: None, mime_type: None, }], auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported, }]; let cell = new_mcp_tools_output_from_statuses(&statuses, McpServerStatusDetail::Full); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn empty_agent_message_cell_transcript() { let cell = AgentMessageCell::new(vec![Line::default()], /*is_first_line*/ false); assert_eq!(cell.transcript_lines(/*width*/ 80), vec![Line::from(" ")]); assert_eq!(cell.desired_transcript_height(/*width*/ 80), 1);}#[test]fn prefixed_wrapped_history_cell_indents_wrapped_lines() { let summary = Line::from(vec![ "You ".into(), "approved".bold(), " codex to run ".into(), "echo something really long to ensure wrapping happens".dim(), " this time".bold(), ]); let cell = PrefixedWrappedHistoryCell::new(summary, "✔ ".green(), " "); let rendered = render_lines(&cell.display_lines(/*width*/ 24)); assert_eq!( rendered, vec![ "✔ You approved codex to".to_string(), " run echo something".to_string(), " really long to ensure".to_string(), " wrapping happens this".to_string(), " time".to_string(), ] );}#[test]fn prefixed_wrapped_history_cell_does_not_split_url_like_token() { let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890"; let cell = PrefixedWrappedHistoryCell::new(Line::from(url_like), "✔ ".green(), " "); let rendered = render_lines(&cell.display_lines(/*width*/ 24)); assert_eq!( rendered .iter() .filter(|line| line.contains(url_like)) .count(), 1, "expected full URL-like token in one rendered line, got: {rendered:?}" );}#[test]fn unified_exec_interaction_cell_does_not_split_url_like_stdin_token() { let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890"; let cell = UnifiedExecInteractionCell::new(Some("true".to_string()), url_like.to_string()); let rendered = render_lines(&cell.display_lines(/*width*/ 24)); assert_eq!( rendered .iter() .filter(|line| line.contains(url_like)) .count(), 1, "expected full URL-like token in one rendered line, got: {rendered:?}" );}#[test]fn prefixed_wrapped_history_cell_height_matches_wrapped_rendering() { let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path"; let cell: Box<dyn HistoryCell> = Box::new(PrefixedWrappedHistoryCell::new( Line::from(url_like), "✔ ".green(), " ", )); let width: u16 = 24; let logical_height = cell.display_lines(width).len() as u16; let wrapped_height = cell.desired_height(width); assert!( wrapped_height > logical_height, "expected wrapped height to exceed logical line count ({logical_height}), got {wrapped_height}" ); let area = Rect::new(0, 0, width, wrapped_height); let mut buf = ratatui::buffer::Buffer::empty(area); cell.render(area, &mut buf); let first_row = (0..area.width) .map(|x| { let symbol = buf[(x, 0)].symbol(); if symbol.is_empty() { ' ' } else { symbol.chars().next().unwrap_or(' ') } }) .collect::<String>(); assert!( first_row.contains("✔"), "expected first rendered row to keep the prefix visible, got: {first_row:?}" );}#[test]fn unified_exec_interaction_cell_height_matches_wrapped_rendering() { let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path"; let cell: Box<dyn HistoryCell> = Box::new(UnifiedExecInteractionCell::new( Some("true".to_string()), url_like.to_string(), )); let width: u16 = 24; let logical_height = cell.display_lines(width).len() as u16; let wrapped_height = cell.desired_height(width); assert!( wrapped_height > logical_height, "expected wrapped height to exceed logical line count ({logical_height}), got {wrapped_height}" ); let area = Rect::new(0, 0, width, wrapped_height); let mut buf = ratatui::buffer::Buffer::empty(area); cell.render(area, &mut buf); let first_row = (0..area.width) .map(|x| { let symbol = buf[(x, 0)].symbol(); if symbol.is_empty() { ' ' } else { symbol.chars().next().unwrap_or(' ') } }) .collect::<String>(); assert!( first_row.contains("Interacted with"), "expected first rendered row to keep the header visible, got: {first_row:?}" );}#[test]fn web_search_history_cell_snapshot() { let query = "example search query with several generic words to exercise wrapping".to_string(); let cell = new_web_search_call( "call-1".to_string(), query.clone(), WebSearchAction::Search { query: Some(query), queries: None, }, ); let rendered = render_lines(&cell.display_lines(/*width*/ 64)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn standalone_unix_update_available_history_cell_snapshot() { let cell = UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::StandaloneUnix)); let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn standalone_windows_update_available_history_cell_snapshot() { let cell = UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::StandaloneWindows)); let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn web_search_history_cell_without_detail_snapshot() { let cell = new_web_search_call("call-1".to_string(), String::new(), WebSearchAction::Other); let rendered = render_lines(&cell.display_lines(/*width*/ 64)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn web_search_history_cell_wraps_with_indented_continuation() { let query = "example search query with several generic words to exercise wrapping".to_string(); let cell = new_web_search_call( "call-1".to_string(), query.clone(), WebSearchAction::Search { query: Some(query), queries: None, }, ); let rendered = render_lines(&cell.display_lines(/*width*/ 64)); assert_eq!( rendered, vec![ "• Searched the web for example search query with several generic".to_string(), " words to exercise wrapping".to_string(), ] );}#[test]fn web_search_history_cell_short_query_does_not_wrap() { let query = "short query".to_string(); let cell = new_web_search_call( "call-1".to_string(), query.clone(), WebSearchAction::Search { query: Some(query), queries: None, }, ); let rendered = render_lines(&cell.display_lines(/*width*/ 64)); assert_eq!( rendered, vec!["• Searched the web for short query".to_string()] );}#[test]fn web_search_history_cell_transcript_snapshot() { let query = "example search query with several generic words to exercise wrapping".to_string(); let cell = new_web_search_call( "call-1".to_string(), query.clone(), WebSearchAction::Search { query: Some(query), queries: None, }, ); let rendered = render_lines(&cell.transcript_lines(/*width*/ 64)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn active_mcp_tool_call_snapshot() { let invocation = McpInvocation { server: "search".into(), tool: "find_docs".into(), arguments: Some(json!({ "query": "ratatui styling", "limit": 3, })), }; let cell = new_active_mcp_tool_call( "call-1".into(), invocation, /*animations_enabled*/ true, ); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn mcp_inventory_loading_snapshot() { let cell = new_mcp_inventory_loading(/*animations_enabled*/ true); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn mcp_inventory_loading_without_animations_is_stable() { let cell = new_mcp_inventory_loading(/*animations_enabled*/ false); let first = render_lines(&cell.display_lines(/*width*/ 80)); let second = render_lines(&cell.display_lines(/*width*/ 80)); assert_eq!(first, second); assert_eq!(first, vec!["• Loading MCP inventory…".to_string()]);}#[test]fn completed_mcp_tool_call_success_snapshot() { let invocation = McpInvocation { server: "search".into(), tool: "find_docs".into(), arguments: Some(json!({ "query": "ratatui styling", "limit": 3, })), }; let result = CallToolResult { content: vec![text_block("Found styling guidance in styles.md")], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-2".into(), invocation, /*animations_enabled*/ true, ); assert!( cell.complete(Duration::from_millis(1420), Ok(result)) .is_none() ); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn completed_mcp_tool_call_image_after_text_returns_extra_cell() { let invocation = McpInvocation { server: "image".into(), tool: "generate".into(), arguments: Some(json!({ "prompt": "tiny image", })), }; let result = CallToolResult { content: vec![ text_block("Here is the image:"), image_block(SMALL_PNG_BASE64), ], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-image".into(), invocation, /*animations_enabled*/ true, ); let extra_cell = cell .complete(Duration::from_millis(25), Ok(result)) .expect("expected image cell"); let rendered = render_lines(&extra_cell.display_lines(/*width*/ 80)); assert_eq!(rendered, vec!["tool result (image output)"]);}#[test]fn completed_mcp_tool_call_accepts_data_url_image_blocks() { let invocation = McpInvocation { server: "image".into(), tool: "generate".into(), arguments: Some(json!({ "prompt": "tiny image", })), }; let data_url = format!("data:image/png;base64,{SMALL_PNG_BASE64}"); let result = CallToolResult { content: vec![image_block(&data_url)], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-image-data-url".into(), invocation, /*animations_enabled*/ true, ); let extra_cell = cell .complete(Duration::from_millis(25), Ok(result)) .expect("expected image cell"); let rendered = render_lines(&extra_cell.display_lines(/*width*/ 80)); assert_eq!(rendered, vec!["tool result (image output)"]);}#[test]fn completed_mcp_tool_call_skips_invalid_image_blocks() { let invocation = McpInvocation { server: "image".into(), tool: "generate".into(), arguments: Some(json!({ "prompt": "tiny image", })), }; let result = CallToolResult { content: vec![image_block("not-base64"), image_block(SMALL_PNG_BASE64)], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-image-2".into(), invocation, /*animations_enabled*/ true, ); let extra_cell = cell .complete(Duration::from_millis(25), Ok(result)) .expect("expected image cell"); let rendered = render_lines(&extra_cell.display_lines(/*width*/ 80)); assert_eq!(rendered, vec!["tool result (image output)"]);}#[test]fn completed_mcp_tool_call_error_snapshot() { let invocation = McpInvocation { server: "search".into(), tool: "find_docs".into(), arguments: Some(json!({ "query": "ratatui styling", "limit": 3, })), }; let mut cell = new_active_mcp_tool_call( "call-3".into(), invocation, /*animations_enabled*/ true, ); assert!( cell.complete(Duration::from_secs(2), Err("network timeout".into())) .is_none() ); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn completed_mcp_tool_call_multiple_outputs_snapshot() { let invocation = McpInvocation { server: "search".into(), tool: "find_docs".into(), arguments: Some(json!({ "query": "ratatui styling", "limit": 3, })), }; let result = CallToolResult { content: vec![ text_block( "Found styling guidance in styles.md and additional notes in CONTRIBUTING.md.", ), resource_link_block( "file:///docs/styles.md", "styles.md", Some("Styles"), Some("Link to styles documentation"), ), ], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-4".into(), invocation, /*animations_enabled*/ true, ); assert!( cell.complete(Duration::from_millis(640), Ok(result)) .is_none() ); let rendered = render_lines(&cell.display_lines(/*width*/ 48)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn completed_mcp_tool_call_wrapped_outputs_snapshot() { let invocation = McpInvocation { server: "metrics".into(), tool: "get_nearby_metric".into(), arguments: Some(json!({ "query": "very_long_query_that_needs_wrapping_to_display_properly_in_the_history", "limit": 1, })), }; let result = CallToolResult { content: vec![text_block( "Line one of the response, which is quite long and needs wrapping.\nLine two continues the response with more detail.", )], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-5".into(), invocation, /*animations_enabled*/ true, ); assert!( cell.complete(Duration::from_millis(1280), Ok(result)) .is_none() ); let rendered = render_lines(&cell.display_lines(/*width*/ 40)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn completed_mcp_tool_call_multiple_outputs_inline_snapshot() { let invocation = McpInvocation { server: "metrics".into(), tool: "summary".into(), arguments: Some(json!({ "metric": "trace.latency", "window": "15m", })), }; let result = CallToolResult { content: vec![ text_block("Latency summary: p50=120ms, p95=480ms."), text_block("No anomalies detected."), ], is_error: None, structured_content: None, meta: None, }; let mut cell = new_active_mcp_tool_call( "call-6".into(), invocation, /*animations_enabled*/ true, ); assert!( cell.complete(Duration::from_millis(320), Ok(result)) .is_none() ); let rendered = render_lines(&cell.display_lines(/*width*/ 120)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn session_header_includes_reasoning_level_when_present() { let cell = SessionHeaderHistoryCell::new( "gpt-4o".to_string(), Some(ReasoningEffortConfig::High), /*show_fast_status*/ true, std::env::temp_dir(), "test", ); let lines = render_lines(&cell.display_lines(/*width*/ 80)); let model_line = lines .iter() .find(|line| line.contains("model:")) .expect("model line"); assert!(model_line.contains("gpt-4o high fast")); assert!(model_line.contains("/model to change"));}#[test]fn session_header_hides_fast_status_when_disabled() { let cell = SessionHeaderHistoryCell::new( "gpt-4o".to_string(), Some(ReasoningEffortConfig::High), /*show_fast_status*/ false, std::env::temp_dir(), "test", ); let lines = render_lines(&cell.display_lines(/*width*/ 80)); let model_line = lines .iter() .find(|line| line.contains("model:")) .expect("model line"); assert!(model_line.contains("gpt-4o high")); assert!(!model_line.contains("fast"));}#[test]#[cfg_attr( target_os = "windows", ignore = "snapshot path rendering differs on Windows")]fn session_header_indicates_yolo_mode() { let cell = SessionHeaderHistoryCell::new( "gpt-5".to_string(), /*reasoning_effort*/ None, /*show_fast_status*/ false, test_path_buf("/tmp/project").abs().to_path_buf(), "test", ) .with_yolo_mode(/*yolo_mode*/ true); let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn yolo_mode_includes_managed_full_access_profiles() { let permission_profile: PermissionProfile = PermissionProfile::Managed { network: NetworkSandboxPolicy::Enabled, file_system: ManagedFileSystemPermissions::Unrestricted, }; assert!(has_yolo_permissions( AskForApproval::Never, &permission_profile ));}#[test]fn yolo_mode_excludes_external_sandbox_profiles() { let permission_profile: PermissionProfile = PermissionProfile::External { network: NetworkSandboxPolicy::Enabled, }; assert!(!has_yolo_permissions( AskForApproval::Never, &permission_profile ));}#[test]fn session_header_directory_center_truncates() { let mut dir = home_dir().expect("home directory"); for part in ["hello", "the", "fox", "is", "very", "fast"] { dir.push(part); } let formatted = SessionHeaderHistoryCell::format_directory_inner(&dir, Some(24)); let sep = std::path::MAIN_SEPARATOR; let expected = format!("~{sep}hello{sep}the{sep}…{sep}very{sep}fast"); assert_eq!(formatted, expected);}#[test]fn session_header_directory_front_truncates_long_segment() { let mut dir = home_dir().expect("home directory"); dir.push("supercalifragilisticexpialidocious"); let formatted = SessionHeaderHistoryCell::format_directory_inner(&dir, Some(18)); let sep = std::path::MAIN_SEPARATOR; let expected = format!("~{sep}…cexpialidocious"); assert_eq!(formatted, expected);}#[test]fn coalesces_sequential_reads_within_one_call() { // Build one exec cell with a Search followed by two Reads let call_id = "c1".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), "echo".into()], parsed: vec![ ParsedCommand::Search { query: Some("shimmer_spans".into()), path: None, cmd: "rg shimmer_spans".into(), }, ParsedCommand::Read { name: "shimmer.rs".into(), cmd: "cat shimmer.rs".into(), path: "shimmer.rs".into(), }, ParsedCommand::Read { name: "status_indicator_widget.rs".into(), cmd: "cat status_indicator_widget.rs".into(), path: "status_indicator_widget.rs".into(), }, ], output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); // Mark call complete so markers are ✓ cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn coalesces_reads_across_multiple_calls() { let mut cell = ExecCell::new( ExecCall { call_id: "c1".to_string(), command: vec!["bash".into(), "-lc".into(), "echo".into()], parsed: vec![ParsedCommand::Search { query: Some("shimmer_spans".into()), path: None, cmd: "rg shimmer_spans".into(), }], output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); // Call 1: Search only cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1)); // Call 2: Read A cell = cell .with_added_call( "c2".into(), vec!["bash".into(), "-lc".into(), "echo".into()], vec![ParsedCommand::Read { name: "shimmer.rs".into(), cmd: "cat shimmer.rs".into(), path: "shimmer.rs".into(), }], ExecCommandSource::Agent, /*interaction_input*/ None, ) .unwrap(); cell.complete_call("c2", CommandOutput::default(), Duration::from_millis(1)); // Call 3: Read B cell = cell .with_added_call( "c3".into(), vec!["bash".into(), "-lc".into(), "echo".into()], vec![ParsedCommand::Read { name: "status_indicator_widget.rs".into(), cmd: "cat status_indicator_widget.rs".into(), path: "status_indicator_widget.rs".into(), }], ExecCommandSource::Agent, /*interaction_input*/ None, ) .unwrap(); cell.complete_call("c3", CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn coalesced_reads_dedupe_names() { let mut cell = ExecCell::new( ExecCall { call_id: "c1".to_string(), command: vec!["bash".into(), "-lc".into(), "echo".into()], parsed: vec![ ParsedCommand::Read { name: "auth.rs".into(), cmd: "cat auth.rs".into(), path: "auth.rs".into(), }, ParsedCommand::Read { name: "auth.rs".into(), cmd: "cat auth.rs".into(), path: "auth.rs".into(), }, ParsedCommand::Read { name: "shimmer.rs".into(), cmd: "cat shimmer.rs".into(), path: "shimmer.rs".into(), }, ], output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn multiline_command_wraps_with_extra_indent_on_subsequent_lines() { // Create a completed exec cell with a multiline command let cmd = "set -o pipefail\ncargo test -p codex-tui --quiet".to_string(); let call_id = "c1".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), cmd], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); // Mark call complete so it renders as "Ran" cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); // Small width to keep the wrapped continuation-indent path covered. let width: u16 = 28; let lines = cell.display_lines(width); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn single_line_command_compact_when_fits() { let call_id = "c1".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["echo".into(), "ok".into()], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); // Wide enough that it fits inline let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn single_line_command_wraps_with_four_space_continuation() { let call_id = "c1".to_string(); let long = "a_very_long_token_without_spaces_to_force_wrapping".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), long], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 24); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn multiline_command_without_wrap_uses_branch_then_eight_spaces() { let call_id = "c1".to_string(); let cmd = "echo one\necho two".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), cmd], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn multiline_command_both_lines_wrap_with_correct_prefixes() { let call_id = "c1".to_string(); let cmd = "first_token_is_long_enough_to_wrap\nsecond_token_is_also_long_enough_to_wrap".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), cmd], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1)); let lines = cell.display_lines(/*width*/ 28); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn stderr_tail_more_than_five_lines_snapshot() { // Build an exec cell with a non-zero exit and 10 lines on stderr to exercise // the head/tail rendering and gutter prefixes. let call_id = "c_err".to_string(); let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), "seq 1 10 1>&2 && false".into()], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); let stderr: String = (1..=10) .map(|n| n.to_string()) .collect::<Vec<_>>() .join("\n"); cell.complete_call( &call_id, CommandOutput { exit_code: 1, formatted_output: String::new(), aggregated_output: stderr, }, Duration::from_millis(1), ); let rendered = cell .display_lines(/*width*/ 80) .iter() .map(|l| { l.spans .iter() .map(|s| s.content.as_ref()) .collect::<String>() }) .collect::<Vec<_>>() .join("\n"); insta::assert_snapshot!(rendered);}#[test]fn ran_cell_multiline_with_stderr_snapshot() { // Build an exec cell that completes (so it renders as "Ran") with a // command long enough that it must render on its own line under the // header, and include a couple of stderr lines to verify the output // block prefixes and wrapping. let call_id = "c_wrap_err".to_string(); let long_cmd = "echo this_is_a_very_long_single_token_that_will_wrap_across_the_available_width"; let mut cell = ExecCell::new( ExecCall { call_id: call_id.clone(), command: vec!["bash".into(), "-lc".into(), long_cmd.to_string()], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }, /*animations_enabled*/ true, ); let stderr = "error: first line on stderr\nerror: second line on stderr".to_string(); cell.complete_call( &call_id, CommandOutput { exit_code: 1, formatted_output: String::new(), aggregated_output: stderr, }, Duration::from_millis(5), ); // Narrow width to force the command to render under the header line. let width: u16 = 28; let rendered = cell .display_lines(width) .iter() .map(|l| { l.spans .iter() .map(|s| s.content.as_ref()) .collect::<String>() }) .collect::<Vec<_>>() .join("\n"); insta::assert_snapshot!(rendered);}#[test]fn user_history_cell_wraps_and_prefixes_each_line_snapshot() { let msg = "one two three four five six seven"; let cell = UserHistoryCell { message: msg.to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: Vec::new(), }; // Small width to force wrapping more clearly. Effective wrap width is width-2 due to the ▌ prefix and trailing space. let width: u16 = 12; let lines = cell.display_lines(width); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn user_history_cell_renders_remote_image_urls() { let cell = UserHistoryCell { message: "describe these".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: vec!["https://example.com/example.png".to_string()], }; let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); assert!(rendered.contains("[Image #1]")); assert!(rendered.contains("describe these")); insta::assert_snapshot!(rendered);}#[test]fn user_history_cell_summarizes_inline_data_urls() { let cell = UserHistoryCell { message: "describe inline image".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: vec!["data:image/png;base64,aGVsbG8=".to_string()], }; let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); assert!(rendered.contains("[Image #1]")); assert!(rendered.contains("describe inline image"));}#[test]fn user_history_cell_numbers_multiple_remote_images() { let cell = UserHistoryCell { message: "describe both".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: vec![ "https://example.com/one.png".to_string(), "https://example.com/two.png".to_string(), ], }; let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); assert!(rendered.contains("[Image #1]")); assert!(rendered.contains("[Image #2]")); insta::assert_snapshot!(rendered);}#[test]fn user_history_cell_height_matches_rendered_lines_with_remote_images() { let cell = UserHistoryCell { message: "line one\nline two".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: vec![ "https://example.com/one.png".to_string(), "https://example.com/two.png".to_string(), ], }; let width = 80; let rendered_len: u16 = cell .display_lines(width) .len() .try_into() .unwrap_or(u16::MAX); assert_eq!(cell.desired_height(width), rendered_len); assert_eq!(cell.desired_transcript_height(width), rendered_len);}#[test]fn user_history_cell_trims_trailing_blank_message_lines() { let cell = UserHistoryCell { message: "line one\n\n \n\t \n".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: vec!["https://example.com/one.png".to_string()], }; let rendered = render_lines(&cell.display_lines(/*width*/ 80)); let trailing_blank_count = rendered .iter() .rev() .take_while(|line| line.trim().is_empty()) .count(); assert_eq!(trailing_blank_count, 1); assert!(rendered.iter().any(|line| line.contains("line one")));}#[test]fn user_history_cell_trims_trailing_blank_message_lines_with_text_elements() { let message = "tokenized\n\n\n".to_string(); let cell = UserHistoryCell { message, text_elements: vec![TextElement::new( (0..8).into(), Some("tokenized".to_string()), )], local_image_paths: Vec::new(), remote_image_urls: vec!["https://example.com/one.png".to_string()], }; let rendered = render_lines(&cell.display_lines(/*width*/ 80)); let trailing_blank_count = rendered .iter() .rev() .take_while(|line| line.trim().is_empty()) .count(); assert_eq!(trailing_blank_count, 1); assert!(rendered.iter().any(|line| line.contains("tokenized")));}#[test]fn render_uses_wrapping_for_long_url_like_line() { let url = "https://example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path/that/keeps/going/for/testing/purposes-only-and-does/not/need/to/resolve/index.html?session_id=abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"; let cell: Box<dyn HistoryCell> = Box::new(UserHistoryCell { message: url.to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: Vec::new(), }); let width: u16 = 52; let height = cell.desired_height(width); assert!( height > 1, "expected wrapped height for long URL, got {height}" ); let area = Rect::new(0, 0, width, height); let mut buf = ratatui::buffer::Buffer::empty(area); cell.render(area, &mut buf); let rendered = (0..area.height) .map(|y| { (0..area.width) .map(|x| { let symbol = buf[(x, y)].symbol(); if symbol.is_empty() { ' ' } else { symbol.chars().next().unwrap_or(' ') } }) .collect::<String>() }) .collect::<Vec<_>>(); let rendered_blob = rendered.join("\n"); assert!( rendered_blob.contains("session_id=abc123"), "expected URL tail to be visible after wrapping, got:\n{rendered_blob}" ); let non_empty_rows = rendered.iter().filter(|row| !row.trim().is_empty()).count() as u16; assert!( non_empty_rows > 3, "expected long URL to span multiple visible rows, got:\n{rendered_blob}" );}#[test]fn plan_update_with_note_and_wrapping_snapshot() { // Long explanation forces wrapping; include long step text to verify step wrapping and alignment. let update = UpdatePlanArgs { explanation: Some( "I’ll update Grafana call error handling by adding retries and clearer messages when the backend is unreachable." .to_string(), ), plan: vec![ PlanItemArg { step: "Investigate existing error paths and logging around HTTP timeouts".into(), status: StepStatus::Completed, }, PlanItemArg { step: "Harden Grafana client error handling with retry/backoff and user‑friendly messages".into(), status: StepStatus::InProgress, }, PlanItemArg { step: "Add tests for transient failure scenarios and surfacing to the UI".into(), status: StepStatus::Pending, }, ], }; let cell = new_plan_update(update); // Narrow width to force wrapping for both the note and steps let lines = cell.display_lines(/*width*/ 32); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn plan_update_without_note_snapshot() { let update = UpdatePlanArgs { explanation: None, plan: vec![ PlanItemArg { step: "Define error taxonomy".into(), status: StepStatus::InProgress, }, PlanItemArg { step: "Implement mapping to user messages".into(), status: StepStatus::Pending, }, ], }; let cell = new_plan_update(update); let lines = cell.display_lines(/*width*/ 40); let rendered = render_lines(&lines).join("\n"); insta::assert_snapshot!(rendered);}#[test]fn plan_update_does_not_split_url_like_tokens_in_note_or_step() { let note_url = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890"; let step_url = "example.test/api/v1/projects/beta-team/releases/2026-02-17/builds/0987654321/artifacts/reports/performance"; let update = UpdatePlanArgs { explanation: Some(format!( "Investigate failures under {note_url} immediately." )), plan: vec![PlanItemArg { step: format!("Validate callbacks under {step_url} before rollout."), status: StepStatus::InProgress, }], }; let cell = new_plan_update(update); let rendered = render_lines(&cell.display_lines(/*width*/ 30)); assert_eq!( rendered .iter() .filter(|line| line.contains(note_url)) .count(), 1, "expected full note URL-like token in one rendered line, got: {rendered:?}" ); assert_eq!( rendered .iter() .filter(|line| line.contains(step_url)) .count(), 1, "expected full step URL-like token in one rendered line, got: {rendered:?}" );}#[test]fn reasoning_summary_block() { let cell = new_reasoning_summary_block( "**High level reasoning**\n\nDetailed reasoning goes here.".to_string(), &test_cwd(), ); let rendered_display = render_lines(&cell.display_lines(/*width*/ 80)); assert_eq!(rendered_display, vec!["• Detailed reasoning goes here."]); let rendered_transcript = render_transcript(cell.as_ref()); assert_eq!(rendered_transcript, vec!["• Detailed reasoning goes here."]);}#[test]fn reasoning_summary_height_matches_wrapped_rendering_for_url_like_content() { let summary = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path/that/keeps/going"; let cell: Box<dyn HistoryCell> = Box::new(ReasoningSummaryCell::new( "High level reasoning".to_string(), summary.to_string(), &test_cwd(), /*transcript_only*/ false, )); let width: u16 = 24; let logical_height = cell.display_lines(width).len() as u16; let wrapped_height = cell.desired_height(width); let expected_wrapped_height = Paragraph::new(Text::from(cell.display_lines(width))) .wrap(Wrap { trim: false }) .line_count(width) as u16; assert_eq!(wrapped_height, expected_wrapped_height); assert!( wrapped_height >= logical_height, "expected wrapped height to be at least logical line count ({logical_height}), got {wrapped_height}" ); let wrapped_transcript_height = cell.desired_transcript_height(width); assert_eq!(wrapped_transcript_height, wrapped_height); let area = Rect::new(0, 0, width, wrapped_height); let mut buf = ratatui::buffer::Buffer::empty(area); cell.render(area, &mut buf); let first_row = (0..area.width) .map(|x| { let symbol = buf[(x, 0)].symbol(); if symbol.is_empty() { ' ' } else { symbol.chars().next().unwrap_or(' ') } }) .collect::<String>(); assert!( first_row.contains("•"), "expected first rendered row to keep summary bullet visible, got: {first_row:?}" );}#[test]fn reasoning_summary_block_returns_reasoning_cell_when_feature_disabled() { let cell = new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &test_cwd()); let rendered = render_transcript(cell.as_ref()); assert_eq!(rendered, vec!["• Detailed reasoning goes here."]);}#[tokio::test]async fn reasoning_summary_block_respects_config_overrides() { let mut config = test_config().await; config.model = Some("gpt-3.5-turbo".to_string()); config.model_supports_reasoning_summaries = Some(true); let cell = new_reasoning_summary_block( "**High level reasoning**\n\nDetailed reasoning goes here.".to_string(), &test_cwd(), ); let rendered_display = render_lines(&cell.display_lines(/*width*/ 80)); assert_eq!(rendered_display, vec!["• Detailed reasoning goes here."]);}#[test]fn reasoning_summary_block_falls_back_when_header_is_missing() { let cell = new_reasoning_summary_block( "**High level reasoning without closing".to_string(), &test_cwd(), ); let rendered = render_transcript(cell.as_ref()); assert_eq!(rendered, vec!["• **High level reasoning without closing"]);}#[test]fn reasoning_summary_block_falls_back_when_summary_is_missing() { let cell = new_reasoning_summary_block( "**High level reasoning without closing**".to_string(), &test_cwd(), ); let rendered = render_transcript(cell.as_ref()); assert_eq!(rendered, vec!["• High level reasoning without closing"]); let cell = new_reasoning_summary_block( "**High level reasoning without closing**\n\n ".to_string(), &test_cwd(), ); let rendered = render_transcript(cell.as_ref()); assert_eq!(rendered, vec!["• High level reasoning without closing"]);}#[test]fn reasoning_summary_block_splits_header_and_summary_when_present() { let cell = new_reasoning_summary_block( "**High level plan**\n\nWe should fix the bug next.".to_string(), &test_cwd(), ); let rendered_display = render_lines(&cell.display_lines(/*width*/ 80)); assert_eq!(rendered_display, vec!["• We should fix the bug next."]); let rendered_transcript = render_transcript(cell.as_ref()); assert_eq!(rendered_transcript, vec!["• We should fix the bug next."]);}#[test]fn deprecation_notice_renders_summary_with_details() { let cell = new_deprecation_notice( "Feature flag `foo`".to_string(), Some("Use flag `bar` instead.".to_string()), ); let lines = cell.display_lines(/*width*/ 80); let rendered = render_lines(&lines); assert_eq!( rendered, vec![ "⚠ Feature flag `foo`".to_string(), "Use flag `bar` instead.".to_string(), ] );}#[test]fn agent_markdown_cell_renders_source_at_different_widths() { let source = "A long agent message that should wrap differently when the terminal width changes.\n"; let cell = AgentMarkdownCell::new(source.to_string(), &test_cwd()); let lines_80 = render_lines(&cell.display_lines(/*width*/ 80)); assert!( lines_80.first().is_some_and(|line| line.starts_with("• ")), "first line should start with bullet prefix: {:?}", lines_80[0] ); let lines_32 = render_lines(&cell.display_lines(/*width*/ 32)); assert!( lines_32.len() > lines_80.len(), "narrower width should produce more wrapped lines: {lines_32:?}", );}#[test]fn agent_markdown_cell_does_not_split_words_after_inline_markdown() { let source = "This paragraph is intentionally long so you can inspect soft wrapping behavior while also checking inline formatting like **bold text**, *italic text*, ***bold italic text***, `inline code`, ~~strikethrough~~, a [link to example.com](https://example.com), and a literal path like [README.md](/Users/felipe.coury/code/codex.fcoury-worktrees/README.md) without introducing manual line breaks.\n"; let cell = AgentMarkdownCell::new(source.to_string(), &test_cwd()); let lines = render_lines(&cell.display_lines(/*width*/ 190)); assert!( lines[0].ends_with("inline code,"), "expected wrapping to stop before 'strikethrough': {lines:?}", ); assert!( lines[1].starts_with(" strikethrough,"), "expected the next line to resume with the full word: {lines:?}", );}#[test]fn streamed_agent_list_paragraph_preserves_item_indent_when_wrapped() { let cell = AgentMessageCell::new( vec![ Line::from("1. Correctness issue: server tool-search completions are rejected."), Line::default(), Line::from( " In next_prompt_suggestion.rs, ToolSearchCall records its call id, but a paired output is ignored and suppresses suggestions.", ), ], /*is_first_line*/ true, ); let lines = render_lines(&cell.display_lines(/*width*/ 64)); assert!( lines .iter() .filter(|line| line.contains("paired output") || line.contains("suggestions.")) .all(|line| line.starts_with(" ")), "expected all wrapped paragraph rows to retain the assistant gutter and list indent: {lines:?}", ); insta::assert_snapshot!(lines.join("\n"));}#[test]fn agent_markdown_cell_narrow_width_shows_prefix_only() { let source = "narrow width coverage\n"; let cell = AgentMarkdownCell::new(source.to_string(), &test_cwd()); let lines = render_lines(&cell.display_lines(/*width*/ 2)); assert_eq!(lines, vec!["• ".to_string()]);}#[test]fn wrapped_and_prefixed_cells_handle_tiny_widths() { let user_cell = UserHistoryCell { message: "tiny width coverage for wrapped user history".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: Vec::new(), }; let agent_message_cell = AgentMessageCell::new( vec!["tiny width agent line".into()], /*is_first_line*/ true, ); let reasoning_cell = ReasoningSummaryCell::new( "Plan".to_string(), "Reasoning summary content for tiny widths.".to_string(), &test_cwd(), /*transcript_only*/ false, ); let agent_markdown_cell = AgentMarkdownCell::new("tiny width agent markdown line\n".to_string(), &test_cwd()); for width in 1..=4 { assert!( !user_cell.display_lines(width).is_empty(), "user cell should render at width {width}", ); assert!( !agent_message_cell.display_lines(width).is_empty(), "agent message cell should render at width {width}", ); assert!( !reasoning_cell.display_lines(width).is_empty(), "reasoning cell should render at width {width}", ); assert!( !agent_markdown_cell.display_lines(width).is_empty(), "agent markdown cell should render at width {width}", ); }}#[test]fn render_clears_area_when_cell_content_shrinks() { let area = Rect::new(0, 0, 40, 6); let mut buf = Buffer::empty(area); let first: Box<dyn HistoryCell> = Box::new(PlainHistoryCell::new(vec![ Line::from("STALE ROW 1"), Line::from("STALE ROW 2"), Line::from("STALE ROW 3"), Line::from("STALE ROW 4"), ])); first.render(area, &mut buf); let second: Box<dyn HistoryCell> = Box::new(PlainHistoryCell::new(vec![Line::from("fresh")])); second.render(area, &mut buf); let mut rendered_rows: Vec<String> = Vec::new(); for y in 0..area.height { let mut row = String::new(); for x in 0..area.width { row.push_str(buf.cell((x, y)).expect("cell should exist").symbol()); } rendered_rows.push(row); } assert!( rendered_rows.iter().all(|row| !row.contains("STALE")), "rendered buffer should not retain stale glyphs: {rendered_rows:?}", ); assert!( rendered_rows .first() .is_some_and(|row| row.contains("fresh")), "expected fresh content in first row: {rendered_rows:?}", );}#[test]fn agent_markdown_cell_survives_insert_history_rewrap() { let source = "\ Canary rollout remained at limited traffic longer than planned because p95 latency briefly regressed during cold-cache periods. Regional expansion succeeded with stable error rates, though internal analytics lagged temporarily. "; let cell = AgentMarkdownCell::new(source.to_string(), &test_cwd()); let width: u16 = 80; let lines = cell.display_lines(width); // Simulate what insert_history_lines does: word_wrap_lines with // the terminal width and no indent. let rewrapped = word_wrap_lines(&lines, width as usize); let before = render_lines(&lines); let after = render_lines(&rewrapped); assert_eq!( before, after, "word_wrap_lines should not alter lines that already fit within width" );}/// Simulate the consolidation backward-walk logic from `App::handle_event`/// to verify it correctly identifies and replaces `AgentMessageCell` runs.#[test]fn consolidation_walker_replaces_agent_message_cells() { use std::sync::Arc; // Build a transcript with: [UserCell, AgentMsg(head), AgentMsg(cont), AgentMsg(cont)] let user = Arc::new(UserHistoryCell { message: "hello".to_string(), text_elements: Vec::new(), local_image_paths: Vec::new(), remote_image_urls: Vec::new(), }) as Arc<dyn HistoryCell>; let head = Arc::new(AgentMessageCell::new( vec![Line::from("line 1")], /*is_first_line*/ true, )) as Arc<dyn HistoryCell>; let cont1 = Arc::new(AgentMessageCell::new( vec![Line::from("line 2")], /*is_first_line*/ false, )) as Arc<dyn HistoryCell>; let cont2 = Arc::new(AgentMessageCell::new( vec![Line::from("line 3")], /*is_first_line*/ false, )) as Arc<dyn HistoryCell>; let mut transcript_cells: Vec<Arc<dyn HistoryCell>> = vec![user.clone(), head, cont1, cont2]; // Run the same consolidation logic as the handler. let source = "line 1\nline 2\nline 3\n".to_string(); let end = transcript_cells.len(); let mut start = end; while start > 0 && transcript_cells[start - 1].is_stream_continuation() && transcript_cells[start - 1] .as_any() .is::<AgentMessageCell>() { start -= 1; } if start > 0 && transcript_cells[start - 1] .as_any() .is::<AgentMessageCell>() && !transcript_cells[start - 1].is_stream_continuation() { start -= 1; } assert_eq!( start, 1, "should find all 3 agent cells starting at index 1" ); assert_eq!(end, 4); // Splice. let consolidated: Arc<dyn HistoryCell> = Arc::new(AgentMarkdownCell::new(source, &test_cwd())); transcript_cells.splice(start..end, std::iter::once(consolidated)); assert_eq!(transcript_cells.len(), 2, "should be [user, consolidated]"); // Verify first cell is still the user cell. assert!( transcript_cells[0].as_any().is::<UserHistoryCell>(), "first cell should be UserHistoryCell" ); // Verify second cell is AgentMarkdownCell. assert!( transcript_cells[1].as_any().is::<AgentMarkdownCell>(), "second cell should be AgentMarkdownCell" );}