use colored::Colorize;
use unicode_width::UnicodeWidthStr;
use super::colors;
use super::detail::DetailLevel;
use super::icons;
use super::renderer;
fn strip_ansi(s: &str) -> String {
let mut result = String::new();
let mut in_escape = false;
for ch in s.chars() {
if ch == '\x1b' {
in_escape = true;
} else if in_escape && ch == 'm' {
in_escape = false;
} else if !in_escape {
result.push(ch);
}
}
result
}
#[test]
fn all_verb_icons_are_single_column_wide() {
for verb in &["infer", "exec", "fetch", "invoke", "agent"] {
let plain = icons::verb_plain(verb);
let width = UnicodeWidthStr::width(plain);
assert_eq!(
width, 1,
"verb '{}' icon '{}' should be 1 column wide, got {}",
verb, plain, width
);
}
}
#[test]
fn all_verb_icons_are_single_char() {
for verb in &["infer", "exec", "fetch", "invoke", "agent"] {
let plain = icons::verb_plain(verb);
assert_eq!(
plain.chars().count(),
1,
"verb '{}' icon should be 1 char",
verb
);
}
}
#[test]
fn cosmic_palette_characters() {
assert_eq!(icons::verb_plain("infer"), "\u{2727}"); assert_eq!(icons::verb_plain("exec"), "\u{2388}"); assert_eq!(icons::verb_plain("fetch"), "\u{2604}"); assert_eq!(icons::verb_plain("invoke"), "\u{229B}"); assert_eq!(icons::verb_plain("agent"), "\u{274B}"); }
#[test]
fn unknown_verb_gives_fallback_icon() {
let plain = icons::verb_plain("unknown");
assert_eq!(plain, "\u{25CF}"); assert_eq!(UnicodeWidthStr::width(plain), 1);
}
#[test]
fn verb_colored_icons_return_colored_string() {
let colored = icons::verb("infer");
let s = colored.to_string();
assert!(
s.contains('\u{2727}'),
"colored infer icon should contain star"
);
}
#[test]
fn all_subsystem_icons_are_single_column_wide() {
let subsystem_icons = vec![
("provider", "\u{22C8}"), ("mcp", "\u{229E}"), ("guardrail", "\u{22A0}"), ("artifact", "\u{229A}"), ("media", "\u{22A1}"), ("structured", "\u{2B21}"), ("vision", "\u{27D0}"), ("http", "\u{21C4}"), ("retry", "\u{21AF}"), ("agent_meta", "\u{2297}"), ("log", "\u{25AA}"), ];
for (name, ch) in &subsystem_icons {
let width = UnicodeWidthStr::width(*ch);
assert_eq!(
width, 1,
"subsystem '{}' icon '{}' should be 1 column wide, got {}",
name, ch, width
);
}
}
#[test]
fn status_icons_are_single_column_wide() {
let statuses = vec![
("pending", "\u{25CB}"), ("running", "\u{25CF}"), ("success", "\u{2713}"), ("failed", "\u{2717}"), ("skipped", "\u{2298}"), ];
for (name, ch) in &statuses {
let width = UnicodeWidthStr::width(*ch);
assert_eq!(
width, 1,
"status '{}' icon '{}' should be 1 column wide, got {}",
name, ch, width
);
}
}
#[test]
fn tokens_under_1000_are_plain() {
assert_eq!(colors::tokens(0), "0");
assert_eq!(colors::tokens(42), "42");
assert_eq!(colors::tokens(842), "842");
assert_eq!(colors::tokens(999), "999");
}
#[test]
fn tokens_1k_to_10k_have_decimal() {
assert_eq!(colors::tokens(1000), "1.0k");
assert_eq!(colors::tokens(1200), "1.2k");
assert_eq!(colors::tokens(9999), "10.0k");
}
#[test]
fn tokens_above_10k_are_integer_k() {
assert_eq!(colors::tokens(10_000), "10k");
assert_eq!(colors::tokens(15_000), "15k");
assert_eq!(colors::tokens(100_000), "100k");
assert_eq!(colors::tokens(999_999), "999k");
assert_eq!(colors::tokens(1_000_000), "1.0M");
assert_eq!(colors::tokens(1_500_000), "1.5M");
assert_eq!(colors::tokens(10_000_000), "10.0M");
}
#[test]
fn format_bytes_under_1kb() {
assert_eq!(renderer::format_bytes(0), "0 B");
assert_eq!(renderer::format_bytes(500), "500 B");
assert_eq!(renderer::format_bytes(1023), "1023 B");
}
#[test]
fn format_bytes_kilobytes() {
assert_eq!(renderer::format_bytes(1024), "1.0 KB");
assert_eq!(renderer::format_bytes(1536), "1.5 KB");
assert_eq!(renderer::format_bytes(10240), "10.0 KB");
}
#[test]
fn format_bytes_megabytes() {
assert_eq!(renderer::format_bytes(1024 * 1024), "1.0 MB");
assert_eq!(renderer::format_bytes(1_500_000), "1.4 MB");
assert_eq!(renderer::format_bytes(10 * 1024 * 1024), "10.0 MB");
}
#[test]
fn json_preview_truncation() {
let json = r#"{"key":"value","long":"very long string here that goes on and on"}"#;
let preview = colors::json_preview(json, 20);
assert!(preview.contains("key"), "preview should contain 'key'");
assert!(
preview.contains('\u{2026}'),
"truncated preview should contain ellipsis"
);
}
#[test]
fn json_preview_no_truncation_for_short_input() {
let json = r#"{"a":1}"#;
let preview = colors::json_preview(json, 100);
let stripped = strip_ansi(&preview);
assert!(stripped.contains("\"a\""));
assert!(stripped.contains("1"));
assert!(!stripped.contains('\u{2026}')); }
#[test]
fn json_preview_no_panic_on_multibyte_utf8() {
let json = r#"{"title":"résumé","café":"latte"}"#;
let preview = colors::json_preview(json, 15);
assert!(!preview.is_empty());
let _ = preview.chars().count();
}
#[test]
fn json_preview_unicode_boundary_safety() {
let json = r#"{"emoji":"hello"}"#;
for max in 1..=20 {
let preview = colors::json_preview(json, max);
let _ = preview.chars().count();
}
}
#[test]
fn json_preview_empty_input() {
let preview = colors::json_preview("", 20);
let stripped = strip_ansi(&preview);
assert!(stripped.is_empty() || stripped.len() < 5);
}
#[test]
fn json_preview_contains_syntax_colors() {
let json = r#"{"name":"value"}"#;
let preview = colors::json_preview(json, 100);
assert!(
preview.contains('\x1b'),
"json_preview should contain ANSI color codes"
);
assert!(preview.contains("\x1b[34m"), "keys should be blue");
assert!(
preview.contains("\x1b[32m"),
"string values should be green"
);
}
#[test]
fn json_preview_numbers_are_yellow() {
let json = r#"{"count":42}"#;
let preview = colors::json_preview(json, 100);
assert!(preview.contains("\x1b[33m"), "numbers should be yellow");
}
#[test]
fn budget_bar_contains_percentage_text() {
let bar_30 = colors::budget_bar(30.0, 20);
assert!(
strip_ansi(&bar_30).contains("30%"),
"bar should contain '30%'"
);
let bar_75 = colors::budget_bar(75.0, 20);
assert!(
strip_ansi(&bar_75).contains("75%"),
"bar should contain '75%'"
);
let bar_95 = colors::budget_bar(95.0, 20);
assert!(
strip_ansi(&bar_95).contains("95%"),
"bar should contain '95%'"
);
}
#[test]
fn budget_bar_zero_and_full() {
let zero = colors::budget_bar(0.0, 20);
assert!(strip_ansi(&zero).contains("0%"));
let full = colors::budget_bar(100.0, 20);
assert!(strip_ansi(&full).contains("100%"));
}
#[test]
fn budget_bar_green_threshold() {
let bar = colors::budget_bar(30.0, 20);
let stripped = strip_ansi(&bar);
assert!(stripped.contains("30%"));
let filled_count = stripped.chars().filter(|&c| c == '\u{2593}').count();
assert_eq!(filled_count, 6, "30% of 20 = 6 filled blocks");
}
#[test]
fn budget_bar_yellow_threshold() {
let bar = colors::budget_bar(75.0, 20);
let stripped = strip_ansi(&bar);
assert!(stripped.contains("75%"));
let filled_count = stripped.chars().filter(|&c| c == '\u{2593}').count();
assert_eq!(filled_count, 15, "75% of 20 = 15 filled blocks");
}
#[test]
fn budget_bar_red_threshold() {
let bar = colors::budget_bar(95.0, 20);
let stripped = strip_ansi(&bar);
assert!(stripped.contains("95%"));
let filled_count = stripped.chars().filter(|&c| c == '\u{2593}').count();
assert_eq!(filled_count, 19, "95% of 20 = 19 filled blocks");
}
#[test]
fn budget_bar_width_matches() {
for pct in [0.0, 25.0, 50.0, 75.0, 100.0] {
let bar = colors::budget_bar(pct, 20);
let stripped = strip_ansi(&bar);
let blocks: usize = stripped
.chars()
.filter(|&c| c == '\u{2593}' || c == '\u{2591}')
.count();
assert_eq!(blocks, 20, "bar width should be 20 at {}%", pct);
}
}
#[test]
fn pad_colored_reaches_target_width() {
let cs = "hello".green();
let padded = colors::pad_colored(&cs, 20);
let visible = colors::stripped_len(&padded);
assert_eq!(visible, 20, "padded string should be 20 visible chars wide");
}
#[test]
fn pad_colored_no_truncation_when_already_wide() {
let cs = "this is a long string".cyan();
let padded = colors::pad_colored(&cs, 5);
let visible = colors::stripped_len(&padded);
assert!(
visible >= 21,
"pad_colored should not truncate, visible={}",
visible
);
}
#[test]
fn pad_colored_exact_width() {
let cs = "12345".normal();
let padded = colors::pad_colored(&cs, 5);
let visible = colors::stripped_len(&padded);
assert_eq!(
visible, 5,
"should be exactly 5 when text is already 5 chars"
);
}
#[test]
fn pad_colored_empty_string() {
let cs = "".normal();
let padded = colors::pad_colored(&cs, 10);
let visible = colors::stripped_len(&padded);
assert_eq!(visible, 10, "padding empty string to width 10");
}
#[test]
fn stripped_len_plain_text() {
assert_eq!(colors::stripped_len("hello"), 5);
assert_eq!(colors::stripped_len(""), 0);
assert_eq!(colors::stripped_len("ab cd"), 5);
}
#[test]
fn stripped_len_with_colored_crate() {
let s = "test".green().bold().to_string();
let len = colors::stripped_len(&s);
assert!(
len >= 4,
"stripped_len should count at least the visible text"
);
}
#[test]
fn stripped_len_consistent_with_pad_colored() {
let cs = "hello world".green();
let padded = colors::pad_colored(&cs, 30);
let visible = colors::stripped_len(&padded);
assert_eq!(visible, 30, "pad + strip should agree on width");
}
#[test]
fn stripped_len_handles_json_preview_output() {
let preview = colors::json_preview(r#"{"a":1}"#, 100);
let len = colors::stripped_len(&preview);
assert!(
len >= 7,
"should count at least the JSON content, got {}",
len
);
}
#[test]
fn detail_level_max_shows_everything() {
let d = DetailLevel::Max;
assert!(d.show_sub_events());
assert!(d.show_previews());
assert!(d.show_sparklines());
assert!(d.show_full_summary());
assert!(d.show_layer_separators());
assert!(d.show_template_events());
assert!(!d.is_json());
}
#[test]
fn detail_level_default_shows_sub_events_but_not_previews() {
let d = DetailLevel::Default;
assert!(d.show_sub_events());
assert!(!d.show_previews());
assert!(!d.show_sparklines());
assert!(d.show_full_summary());
assert!(!d.show_layer_separators());
assert!(!d.show_template_events());
assert!(!d.is_json());
}
#[test]
fn detail_level_min_shows_nothing_extra() {
let d = DetailLevel::Min;
assert!(!d.show_sub_events());
assert!(!d.show_previews());
assert!(!d.show_sparklines());
assert!(!d.show_full_summary());
assert!(!d.show_layer_separators());
assert!(!d.show_template_events());
assert!(!d.is_json());
}
#[test]
fn detail_level_json_is_special() {
let d = DetailLevel::Json;
assert!(d.is_json());
assert!(!d.show_sub_events());
assert!(!d.show_previews());
assert!(!d.show_sparklines());
assert!(!d.show_full_summary());
}
#[test]
fn detail_level_default_variant_is_max() {
assert_eq!(DetailLevel::default(), DetailLevel::Max);
}
#[test]
fn detail_level_display_roundtrip() {
for variant in &[
DetailLevel::Max,
DetailLevel::Default,
DetailLevel::Min,
DetailLevel::Json,
] {
let s = variant.to_string();
let parsed: DetailLevel = s.parse().unwrap();
assert_eq!(*variant, parsed, "roundtrip failed for {:?}", variant);
}
}
#[test]
fn detail_level_parse_case_insensitive() {
assert_eq!("MAX".parse::<DetailLevel>().unwrap(), DetailLevel::Max);
assert_eq!("Json".parse::<DetailLevel>().unwrap(), DetailLevel::Json);
assert_eq!("MIN".parse::<DetailLevel>().unwrap(), DetailLevel::Min);
}
#[test]
fn detail_level_parse_invalid() {
assert!("verbose".parse::<DetailLevel>().is_err());
assert!("".parse::<DetailLevel>().is_err());
assert!("quiet".parse::<DetailLevel>().is_err());
}
#[test]
fn duration_sub_millisecond_shows_microseconds() {
let d = colors::duration(0.0005);
let stripped = strip_ansi(&d.to_string());
assert!(
stripped.contains("\u{00B5}s"),
"sub-ms should show microseconds, got: {}",
stripped
);
}
#[test]
fn duration_milliseconds() {
let d = colors::duration(0.5);
let stripped = strip_ansi(&d.to_string());
assert_eq!(stripped, "500ms", "0.5s = 500ms");
}
#[test]
fn duration_seconds() {
let d = colors::duration(2.5);
let stripped = strip_ansi(&d.to_string());
assert_eq!(stripped, "2.5s");
}
#[test]
fn duration_above_60s_shows_minutes() {
let d = colors::duration(90.0);
let stripped = strip_ansi(&d.to_string());
assert_eq!(stripped, "1m30.0s", "90s = 1m30.0s");
}
#[test]
fn duration_text_format_at_boundaries() {
let d = strip_ansi(&colors::duration(0.0005).to_string());
assert!(d.ends_with("\u{00B5}s"), "got: {}", d);
let d = strip_ansi(&colors::duration(0.001).to_string());
assert_eq!(d, "1ms");
let d = strip_ansi(&colors::duration(0.999).to_string());
assert_eq!(d, "999ms");
let d = strip_ansi(&colors::duration(1.0).to_string());
assert_eq!(d, "1.0s");
let d = strip_ansi(&colors::duration(60.0).to_string());
assert_eq!(d, "1m0.0s");
let d = strip_ansi(&colors::duration(125.0).to_string());
assert_eq!(d, "2m5.0s");
}
#[test]
fn duration_color_thresholds() {
let green_dur = colors::duration(0.5);
let green_dbg = format!("{:?}", green_dur);
assert!(
green_dbg.contains("Green") || green_dbg.contains("green"),
"0.5s should be green, debug: {}",
green_dbg
);
let yellow_dur = colors::duration(2.5);
let yellow_dbg = format!("{:?}", yellow_dur);
assert!(
yellow_dbg.contains("Yellow") || yellow_dbg.contains("yellow"),
"2.5s should be yellow, debug: {}",
yellow_dbg
);
let red_dur = colors::duration(10.0);
let red_dbg = format!("{:?}", red_dur);
assert!(
red_dbg.contains("Red") || red_dbg.contains("red"),
"10s should be red, debug: {}",
red_dbg
);
}
#[test]
fn duration_color_at_exact_boundaries() {
let at_1s = format!("{:?}", colors::duration(1.0));
assert!(
at_1s.contains("Yellow") || at_1s.contains("yellow"),
"1.0s should be yellow, debug: {}",
at_1s
);
let at_5s = format!("{:?}", colors::duration(5.0));
assert!(
at_5s.contains("Red") || at_5s.contains("red"),
"5.0s should be red, debug: {}",
at_5s
);
let under_1s = format!("{:?}", colors::duration(0.999));
assert!(
under_1s.contains("Green") || under_1s.contains("green"),
"0.999s should be green, debug: {}",
under_1s
);
}
#[test]
fn sparkline_zero_max_does_not_panic() {
let s = colors::sparkline(0, 0);
let stripped = strip_ansi(&s.to_string());
assert_eq!(
stripped.chars().count(),
8,
"sparkline should be 8 chars wide"
);
}
#[test]
fn sparkline_full_ratio() {
let s = colors::sparkline(100, 100);
let stripped = strip_ansi(&s.to_string());
assert_eq!(stripped.chars().count(), 8);
assert!(
stripped.contains('\u{2588}'),
"full ratio should have full blocks"
);
}
#[test]
fn cost_formatting() {
let tiny = strip_ansi(&colors::cost(0.0001).to_string());
assert!(tiny.starts_with('$'), "cost should start with $");
let small = strip_ansi(&colors::cost(0.005).to_string());
assert!(small.starts_with('$'));
let large = strip_ansi(&colors::cost(1.23).to_string());
assert_eq!(large, "$1.23");
}
#[test]
fn ttft_formatting() {
let fast = strip_ansi(&colors::ttft(100).to_string());
assert_eq!(fast, "100ms");
let medium = strip_ansi(&colors::ttft(300).to_string());
assert_eq!(medium, "300ms");
let slow = strip_ansi(&colors::ttft(600).to_string());
assert_eq!(slow, "600ms");
}