use super::super::action_line::action_text;
use crate::interactive::tui::types::App;
use crate::interactive::tui::ui_snapshot_tests::{empty_app, fixed_status, render_buffer};
use unicode_width::UnicodeWidthStr;
fn busy_stream() -> crate::interactive::tui::agent::Stream {
let (_tx, rx) = tokio::sync::mpsc::unbounded_channel();
crate::interactive::tui::agent::Stream {
rx,
cancel: saya_agent::CancellationToken::new(),
prompt: String::new(),
}
}
fn busy_app(sql: &str) -> App {
let mut app = empty_app();
app.request.stream = Some(busy_stream());
app.request.started = Some(std::time::Instant::now());
app.request.activity = Some("bounded_sql_query".into());
app.transcript.buffer_tool_request(
"bounded_sql_query".into(),
serde_json::json!({ "sql": sql }),
None,
);
app
}
#[test]
fn the_cancel_hint_survives_a_long_cjk_action_at_100_columns() {
let app = busy_app(&"日本".repeat(30));
let buffer = render_buffer(&app, &fixed_status(), 100, 10);
assert!(
buffer.contains("…"),
"precondition: the wide detail must truncate with an ellipsis:\n{buffer}"
);
assert!(
buffer.contains("Esc to cancel"),
"the cancel affordance must paint beside a long CJK action at 100 columns:\n{buffer}"
);
}
#[test]
fn the_cancel_hint_survives_a_long_cjk_action_at_80_columns() {
let app = busy_app(&"日本".repeat(30));
let buffer = render_buffer(&app, &fixed_status(), 80, 10);
assert!(
buffer.contains("Esc to cancel"),
"the cancel affordance must survive an 80-column frame with wide text:\n{buffer}"
);
}
#[test]
fn an_equal_length_ascii_action_still_paints_the_hint() {
let app = busy_app(&"a".repeat(120));
let buffer = render_buffer(&app, &fixed_status(), 100, 10);
assert!(
buffer.contains("…"),
"precondition: the equal-length ASCII detail must truncate too:\n{buffer}"
);
assert!(
buffer.contains("Esc to cancel"),
"the control: an ASCII action of equal cell length must keep the hint:\n{buffer}"
);
}
#[test]
fn a_truncated_detail_never_splits_a_grapheme() {
let mark = "e\u{0301}";
let marked = mark.repeat(20);
let result = action_text(
Some("bounded_sql_query"),
Some((
"bounded_sql_query".into(),
serde_json::json!({ "sql": marked }),
)),
38,
);
assert_eq!(
result,
format!("running bounded_sql_query: {}… ", mark.repeat(9)),
"the cut must keep whole base+mark clusters:\n{result}"
);
let family = "👨\u{200D}👩\u{200D}👦";
let detail = format!("{}{family}b", "a".repeat(8));
let result = action_text(
Some("bounded_sql_query"),
Some((
"bounded_sql_query".into(),
serde_json::json!({ "sql": detail }),
)),
38,
);
assert_eq!(
result,
format!("running bounded_sql_query: {}… ", "a".repeat(8)),
"a grapheme too wide to finish in the budget is dropped whole:\n{result}"
);
assert!(
!result.contains('\u{1F468}') && !result.contains('\u{200D}'),
"no fragment of the family may survive the cut:\n{result}"
);
}
#[test]
fn a_truncated_detail_never_exceeds_its_cell_budget() {
let wide = "日".repeat(50);
let result = action_text(
Some("bounded_sql_query"),
Some((
"bounded_sql_query".into(),
serde_json::json!({ "sql": wide }),
)),
40,
);
assert_eq!(
result,
format!("running bounded_sql_query: {}… ", "日".repeat(5)),
"the cut honours the cell budget exactly:\n{result}"
);
assert!(
result.width() <= 40,
"the painted phrase must fit its 40-cell room ({} cells): {result:?}",
result.width()
);
}