use super::*;
use crate::tui_internal::exec_cell::CommandOutput;
use crate::tui_internal::exec_cell::ExecCall;
use crate::tui_internal::exec_cell::ExecCell;
use crate::tui_internal::legacy_core::config::Config;
use crate::tui_internal::legacy_core::config::ConfigBuilder;
use crate::tui_internal::session_state::ThreadSessionState;
use crate::tui_internal::wrapping::word_wrap_lines;
use lemurclaw_core::app_server_protocol::AskForApproval;
use lemurclaw_core::app_server_protocol::McpAuthStatus;
use lemurclaw_core::config::types::McpServerConfig;
use lemurclaw_core::otel::RuntimeMetricTotals;
use lemurclaw_core::otel::RuntimeMetricsSummary;
use lemurclaw_core::protocol::ThreadId;
use lemurclaw_core::protocol::account::PlanType;
use lemurclaw_core::protocol::error::UnexpectedResponseError;
use lemurclaw_core::protocol::parse_command::ParsedCommand;
use dirs::home_dir;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use reqwest::StatusCode;
use serde_json::json;
use std::collections::HashMap;
use std::path::PathBuf;
use lemurclaw_core::app_server_protocol::CommandExecutionSource as ExecCommandSource;
use lemurclaw_core::protocol::mcp::CallToolResult;
use lemurclaw_core::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 {
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"),
],
false,
);
let lines = cell.display_lines( 80);
insta::assert_snapshot!(render_lines(&lines).join("\n"), @" first
second");
assert_eq!(cell.desired_height( 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(),
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( 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( 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( 80);
assert_eq!(
lines[2].hyperlinks,
vec![crate::tui_internal::terminal_hyperlinks::TerminalHyperlink::web(
0..destination.len(),
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( 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,
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"})),
},
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( 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( 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: lemurclaw_core::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( 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.0,
responses_api_engine_service_tbt_ms: 1_240.0,
turn_ttft_ms: 0,
turn_ttfm_ms: 0,
};
let cell = FinalMessageSeparator::new(Some(12), Some(summary));
let rendered = render_lines(&cell.display_lines( 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 runtime_metrics_label_rounds_fractional_tbt_milliseconds() {
let summary = RuntimeMetricsSummary {
responses_api_engine_iapi_tbt_ms: 2.450638,
responses_api_engine_service_tbt_ms: 5.267279,
..RuntimeMetricsSummary::default()
};
insta::assert_snapshot!(runtime_metrics_label(summary).expect("TBT label"), @"TBT: 2ms (iapi) 5ms (service)");
}
#[test]
fn final_message_separator_includes_worked_label_after_one_minute() {
let cell = FinalMessageSeparator::new(Some(61), None);
let rendered = render_lines(&cell.display_lines( 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( 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"),
false,
Some("Model just became available".to_string()),
Some(PlanType::Free),
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"),
false,
Some("Model just became available".to_string()),
Some(PlanType::Free),
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"),
true,
Some("Model just became available".to_string()),
Some(PlanType::Free),
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"),
false,
Some("Model just became available".to_string()),
Some(PlanType::Free),
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( 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( 80)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn safety_access_block_event_snapshot() {
let cell = new_safety_access_block_event();
let rendered = render_lines(&cell.display_lines( 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( 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( 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( 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( 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( 120)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn error_event_bedrock_expired_signature_snapshot() {
let error = UnexpectedResponseError {
status: StatusCode::UNAUTHORIZED,
body: "Signature expired: 20260609T133205Z is now earlier than 20260614T062525Z \
(20260614T063025Z - 5 min.)"
.to_string(),
user_message: Some(
"Amazon Bedrock rejected the request because its AWS signature has expired. \
Refresh your AWS credentials and retry. If `AWS_BEARER_TOKEN_BEDROCK` is set, update or \
unset it, then restart Codex"
.to_string(),
),
url: Some("https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses".to_string()),
cf_ray: None,
request_id: None,
identity_authorization_error: None,
identity_error_code: None,
};
let cell = new_error_event(error.to_string());
let rendered = render_lines(&cell.display_lines( 100)).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( 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"], 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( 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: lemurclaw_core::app_server_protocol::McpAuthStatus::Unsupported,
}];
let cell =
new_mcp_tools_output_from_statuses(&statuses, McpServerStatusDetail::ToolsAndAuthOnly);
let rendered = render_lines(&cell.display_lines( 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: lemurclaw_core::app_server_protocol::McpAuthStatus::Unsupported,
}];
let cell = new_mcp_tools_output_from_statuses(&statuses, McpServerStatusDetail::Full);
let rendered = render_lines(&cell.display_lines( 120)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn empty_agent_message_cell_transcript() {
let cell = AgentMessageCell::new(vec![Line::default()], false);
assert_eq!(cell.transcript_lines( 80), vec![Line::from(" ")]);
assert_eq!(cell.desired_transcript_height( 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( 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( 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( 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( 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( 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( 110)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn pnpm_update_available_history_cell_snapshot() {
let cell =
UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::PnpmGlobalLatest));
let rendered = render_lines(&cell.display_lines( 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( 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( 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( 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( 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,
true,
);
let rendered = render_lines(&cell.display_lines( 80)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn mcp_inventory_loading_snapshot() {
let cell = new_mcp_inventory_loading( true);
let rendered = render_lines(&cell.display_lines( 80)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn mcp_inventory_loading_without_animations_is_stable() {
let cell = new_mcp_inventory_loading( false);
let first = render_lines(&cell.display_lines( 80));
let second = render_lines(&cell.display_lines( 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,
true,
);
assert!(
cell.complete(Duration::from_millis(1420), Ok(result))
.is_none()
);
let rendered = render_lines(&cell.display_lines( 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,
true,
);
let extra_cell = cell
.complete(Duration::from_millis(25), Ok(result))
.expect("expected image cell");
let rendered = render_lines(&extra_cell.display_lines( 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,
true,
);
let extra_cell = cell
.complete(Duration::from_millis(25), Ok(result))
.expect("expected image cell");
let rendered = render_lines(&extra_cell.display_lines( 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,
true,
);
let extra_cell = cell
.complete(Duration::from_millis(25), Ok(result))
.expect("expected image cell");
let rendered = render_lines(&extra_cell.display_lines( 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,
true,
);
assert!(
cell.complete(Duration::from_secs(2), Err("network timeout".into()))
.is_none()
);
let rendered = render_lines(&cell.display_lines( 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,
true,
);
assert!(
cell.complete(Duration::from_millis(640), Ok(result))
.is_none()
);
let rendered = render_lines(&cell.display_lines( 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,
true,
);
assert!(
cell.complete(Duration::from_millis(1280), Ok(result))
.is_none()
);
let rendered = render_lines(&cell.display_lines( 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,
true,
);
assert!(
cell.complete(Duration::from_millis(320), Ok(result))
.is_none()
);
let rendered = render_lines(&cell.display_lines( 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),
true,
std::env::temp_dir(),
"test",
);
let lines = render_lines(&cell.display_lines( 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),
false,
std::env::temp_dir(),
"test",
);
let lines = render_lines(&cell.display_lines( 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(),
None,
false,
test_path_buf("/tmp/project").abs().to_path_buf(),
"test",
)
.with_yolo_mode( true);
let rendered = render_lines(&cell.display_lines( 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() {
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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 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,
},
true,
);
cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1));
assert!(cell.add_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,
None,
));
cell.complete_call("c2", CommandOutput::default(), Duration::from_millis(1));
assert!(cell.add_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,
None,
));
cell.complete_call("c3", CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 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,
},
true,
);
cell.complete_call("c1", CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 80);
let rendered = render_lines(&lines).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn multiline_command_wraps_with_extra_indent_on_subsequent_lines() {
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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 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,
},
true,
);
cell.complete_call(&call_id, CommandOutput::default(), Duration::from_millis(1));
let lines = cell.display_lines( 28);
let rendered = render_lines(&lines).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn stderr_tail_more_than_five_lines_snapshot() {
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,
},
true,
);
let stderr: String = (1..=10)
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\n");
cell.complete_call(
&call_id,
CommandOutput {
exit_code: 1,
aggregated_output: stderr,
},
Duration::from_millis(1),
);
let rendered = cell
.display_lines( 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() {
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,
},
true,
);
let stderr = "error: first line on stderr\nerror: second line on stderr".to_string();
cell.complete_call(
&call_id,
CommandOutput {
exit_code: 1,
aggregated_output: stderr,
},
Duration::from_millis(5),
);
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 = "_count_r\x1b[13;2:3uows";
let cell = UserHistoryCell {
message: msg.to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
remote_image_urls: Vec::new(),
};
let width: u16 = 12;
let lines = cell.display_lines(width);
let rendered = render_lines(&lines).join("\n");
assert_eq!(render_lines(&cell.raw_lines()), ["_count_rows"]);
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( 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( 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( 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( 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( 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() {
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);
let lines = cell.display_lines( 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( 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( 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(
vec!["**High level reasoning**\n\nDetailed reasoning goes here.".to_string()],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 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(),
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(
vec!["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());
let cell = new_reasoning_summary_block(
vec!["**High level reasoning**\n\nDetailed reasoning goes here.".to_string()],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 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(
vec!["**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(
vec!["**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(
vec!["**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(
vec!["**High level plan**\n\nWe should fix the bug next.".to_string()],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 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 reasoning_summary_block_hides_empty_html_comment_parts() {
let cell = new_reasoning_summary_block(
vec![
"**Checking the first thing**\n\n<!-- -->".to_string(),
"**Checking the second thing**\n\n<!-- -->".to_string(),
],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 80));
insta::assert_snapshot!(rendered_display.join("\n"), @"");
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, Vec::<String>::new());
}
#[test]
fn reasoning_summary_block_preserves_bold_content_after_empty_html_comment_part() {
let cell = new_reasoning_summary_block(
vec![
"**Status**\n\n<!-- -->".to_string(),
"**Important conclusion**".to_string(),
"<!-- -->".to_string(),
],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 80));
insta::assert_snapshot!(rendered_display.join("\n"), @"• Important conclusion");
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, vec!["• Important conclusion"]);
let cell = new_reasoning_summary_block(
vec![
"**Status**\n\n<!-- -->".to_string(),
"**Result:** keep **this**".to_string(),
],
&test_cwd(),
);
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, vec!["• Result: keep this"]);
}
#[test]
fn reasoning_summary_block_strips_header_after_leading_empty_part() {
let cell = new_reasoning_summary_block(
vec![
"**Status**\n\n<!-- -->".to_string(),
"**Checking tests**\n\nTests passed".to_string(),
],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 80));
insta::assert_snapshot!(rendered_display.join("\n"), @"• Tests passed");
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, vec!["• Tests passed"]);
}
#[test]
fn reasoning_summary_block_drops_empty_part_after_real_content() {
let cell = new_reasoning_summary_block(
vec![
"**Plan**\n\ndone".to_string(),
"**Checking tests**\n\n<!-- -->".to_string(),
],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 80));
insta::assert_snapshot!(rendered_display.join("\n"), @"• done");
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, vec!["• done"]);
}
#[test]
fn reasoning_summary_block_preserves_literal_html_comment() {
let cell = new_reasoning_summary_block(
vec!["**Plan**\n\nUse `<!-- -->` in JSX.".to_string()],
&test_cwd(),
);
let rendered_display = render_lines(&cell.display_lines( 80));
insta::assert_snapshot!(rendered_display.join("\n"), @"• Use <!-- --> in JSX.");
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(rendered_transcript, vec!["• Use <!-- --> in JSX."]);
}
#[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( 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( 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( 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( 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.",
),
],
true,
);
let lines = render_lines(&cell.display_lines( 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( 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()],
true,
);
let reasoning_cell = ReasoningSummaryCell::new(
"Plan".to_string(),
"Reasoning summary content for tiny widths.".to_string(),
&test_cwd(),
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);
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"
);
}
#[test]
fn consolidation_walker_replaces_agent_message_cells() {
use std::sync::Arc;
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")],
true,
)) as Arc<dyn HistoryCell>;
let cont1 = Arc::new(AgentMessageCell::new(
vec![Line::from("line 2")],
false,
)) as Arc<dyn HistoryCell>;
let cont2 = Arc::new(AgentMessageCell::new(
vec![Line::from("line 3")],
false,
)) as Arc<dyn HistoryCell>;
let mut transcript_cells: Vec<Arc<dyn HistoryCell>> = vec![user.clone(), head, cont1, cont2];
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);
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]");
assert!(
transcript_cells[0].as_any().is::<UserHistoryCell>(),
"first cell should be UserHistoryCell"
);
assert!(
transcript_cells[1].as_any().is::<AgentMarkdownCell>(),
"second cell should be AgentMarkdownCell"
);
}