use crate::channels::telegram::suggest_options::{
BUTTON_LABEL_MAX_UNITS, FOLLOWUP_PREFIX, MAX_NUMBERS_PER_ROW, SHARED_ROW_MAX_CHARS,
SINGLE_BUTTON_MAX_UNITS, SuggestLayout, append_rows_and_trailer_md, enforce_button_fit,
go_button_label, go_tier_line, go_tier_lines_rich, pick_layout, row_fits,
suggestion_rows_rich_html,
};
fn opts(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn test_short_few_options_share_one_row() {
assert_eq!(
pick_layout(&opts(&["Yes", "No", "Skip"])),
SuggestLayout::SharedRow
);
}
#[test]
fn test_five_short_options_do_not_share_a_row() {
let o = opts(&["alpha", "beta", "gamma", "delta", "eps"]);
assert!(o.iter().all(|s| s.chars().count() <= SHARED_ROW_MAX_CHARS));
assert!(o.len() > MAX_NUMBERS_PER_ROW);
assert_eq!(pick_layout(&o), SuggestLayout::Column);
}
#[test]
fn test_one_long_label_kills_the_shared_row() {
let o = vec!["Yes".to_string(), "x".repeat(SHARED_ROW_MAX_CHARS + 1)];
assert_eq!(pick_layout(&o), SuggestLayout::Column);
}
#[test]
fn test_the_button_width_boundary_is_exclusive() {
assert_eq!(
pick_layout(&["x".repeat(BUTTON_LABEL_MAX_UNITS)]),
SuggestLayout::Column
);
assert_eq!(
pick_layout(&[
"Ship it".to_string(),
"x".repeat(BUTTON_LABEL_MAX_UNITS + 1)
]),
SuggestLayout::NumberedProse
);
}
#[test]
fn test_single_option_rides_full_width_past_the_shared_budget() {
let label = "Smoke OK — ack both units".to_string(); assert!(label.chars().count() > BUTTON_LABEL_MAX_UNITS);
assert_eq!(pick_layout(&[label]), SuggestLayout::Column);
assert_eq!(
pick_layout(&["x".repeat(SINGLE_BUTTON_MAX_UNITS)]),
SuggestLayout::Column
);
assert_eq!(
pick_layout(&["x".repeat(SINGLE_BUTTON_MAX_UNITS + 1)]),
SuggestLayout::NumberedProse
);
assert_eq!(
pick_layout(&["ok".to_string(), "x".repeat(BUTTON_LABEL_MAX_UNITS + 1)]),
SuggestLayout::NumberedProse
);
}
#[test]
fn test_shared_row_respects_the_total_budget() {
let o = vec![
"x".repeat(SHARED_ROW_MAX_CHARS),
"y".repeat(SHARED_ROW_MAX_CHARS),
];
assert_eq!(pick_layout(&o), SuggestLayout::Column);
}
#[test]
fn test_go_tier_body_lines_carry_the_119_shape() {
let body = go_tier_lines_rich(&opts(&["Ship it", "Review & merge"]));
assert!(
!body.contains("Suggested next"),
"#1204: the lines ride under the answer, no header of their own"
);
assert!(
body.contains("1. Ship it") && body.contains("2. Review & merge"),
"plain numbered list for n>=2, escaping via the shared renderer: {body}"
);
assert!(
!body.contains("Go:") && !body.contains("<b>"),
"no Go-tier artifacts on the n>=2 fold: {body}"
);
assert!(
!body.contains("<ol>"),
"no html <ol> — plain numbered prose: {body}"
);
let single = go_tier_lines_rich(&opts(&["Ship it"]));
assert_eq!(single, "<b>Go: Ship it?</b>");
}
#[test]
fn test_go_tier_dedupes_trailing_question_mark() {
assert_eq!(go_tier_line("Confirm render?"), "**Go: Confirm render?**");
assert_eq!(go_tier_line("Confirm render??"), "**Go: Confirm render??**");
}
#[test]
fn test_go_tier_verb_repeat_rule() {
assert_eq!(
go_tier_line("Go — implement #98 after the #96 gate lands"),
"**Go — implement #98 after the #96 gate lands?**"
);
assert_eq!(
go_tier_line("Smoke OK — ack both units"),
"**Go: Smoke OK — ack both units?**"
);
assert_eq!(go_tier_line("go fast"), "**go fast?**");
assert_eq!(go_tier_line("Gossip about it"), "**Go: Gossip about it?**");
}
#[test]
fn test_go_tier_buttons_carry_their_option_number() {
let token = "ab12cd34";
let html = suggestion_rows_rich_html(&opts(&["Ship it", &"x".repeat(30)]), token);
assert!(
html.contains(&format!(">{}</tg-button>", go_button_label(1, false)))
&& html.contains(&format!(">{}</tg-button>", go_button_label(2, false))),
"one numbered button per option: {html}"
);
assert!(
!html.contains(">Go!<"),
"no Go! buttons on the n>=2 fold: {html}"
);
assert!(
html.contains(&format!("{FOLLOWUP_PREFIX}{token}:0")),
"callback data still routes by index: {html}"
);
assert_eq!(go_button_label(1, true), "Go!");
let single_html = suggestion_rows_rich_html(&opts(&[&"x".repeat(31)]), token);
assert!(
single_html.contains(">Go!</tg-button>"),
"single fold keeps the Go! button: {single_html}"
);
}
#[test]
fn test_callback_data_carries_the_index_not_the_text() {
let token = "ab12cd34";
let html = suggestion_rows_rich_html(&opts(&["Ship it", "Hold"]), token);
for i in 0..2 {
let expected = format!("{FOLLOWUP_PREFIX}{token}:{i}");
assert!(
html.contains(&expected),
"missing callback data {expected} in {html}"
);
assert!(
expected.len() <= 64,
"#1204: callback data must fit Telegram's 64-byte cap, got {}",
expected.len()
);
}
assert!(
!html.contains("Ship it\" data="),
"option text must not ride in callback data"
);
}
#[test]
fn test_callback_data_stays_within_the_cap_at_the_worst_index() {
let token = "ffffffff";
let widest = format!("{FOLLOWUP_PREFIX}{token}:{}", usize::from(u8::MAX));
assert!(
widest.len() <= 64,
"#1204: {} bytes exceeds the callback_data cap",
widest.len()
);
}
#[test]
fn test_enforce_button_fit_ships_fitting_bodies_byte_identical() {
let body = "<p>Готово</p>\n<tg-button-row>\
<tg-button type=\"callback_data\" data=\"followup:tok:0\" \
style=\"primary\">Полёт</tg-button></tg-button-row>";
assert_eq!(enforce_button_fit(body), body);
}
#[test]
fn test_enforce_button_fit_folds_oversized_labels_keeping_routing() {
let long = "Проверка ширины кнопки хххххххххЖЖЖ";
let body = format!(
"<tg-button-row><tg-button type=\"url\" data=\"https://x/{long}\">{long}\
</tg-button></tg-button-row>"
);
let out = enforce_button_fit(&body);
assert!(
out.contains("<tg-button type=\"url\" data=\"https://x/"),
"{out}"
);
assert!(out.contains(">1</tg-button>"), "{out}");
assert!(out.contains("<li>"), "{out}");
assert!(out.contains(long), "{out}");
assert_eq!(enforce_button_fit(&out), out);
}
#[test]
fn test_enforce_button_fit_folds_rows_over_the_total_budget() {
let body = "<tg-button-row>\
<tg-button type=\"callback_data\" data=\"followup:t:0\">Полёт норм!!\
</tg-button><tg-button type=\"callback_data\" data=\"followup:t:1\">\
Всё чётко!!!</tg-button></tg-button-row>";
let out = enforce_button_fit(body);
assert!(
out.contains(">1</tg-button>") && out.contains(">2</tg-button>"),
"{out}"
);
assert!(out.contains("<li>Полёт норм!!</li>"), "{out}");
assert!(out.contains("<li>Всё чётко!!!</li>"), "{out}");
}
#[test]
fn test_row_fits_solo_arm_grants_the_solo_budget() {
let label = "x".repeat(SINGLE_BUTTON_MAX_UNITS);
assert!(row_fits(&[&label]));
assert!(!row_fits(&[&"x".repeat(SINGLE_BUTTON_MAX_UNITS + 1)]));
let confirm = "Smoke OK — ack both units".to_string();
assert!(confirm.chars().count() > BUTTON_LABEL_MAX_UNITS);
assert!(row_fits(&[&confirm]));
}
#[test]
fn test_row_fits_shared_arm_keeps_the_79_caps() {
let a = "x".repeat(SHARED_ROW_MAX_CHARS);
let b = "y".repeat(SHARED_ROW_MAX_CHARS);
assert!(!row_fits(&[&a, &b]));
let short = "x".repeat(SHARED_ROW_MAX_CHARS - 4);
assert!(row_fits(&[&short, &short]));
let long = "x".repeat(SHARED_ROW_MAX_CHARS + 1);
assert!(!row_fits(&[&short, &long]));
}
#[test]
fn test_chokepoint_ships_solo_rows_the_emitter_approved() {
let label = "Smoke OK — ack both units"; let body = format!(
"<tg-button-row><tg-button type=\"callback_data\" data=\"followup:tok:0\" \
style=\"primary\">{label}</tg-button></tg-button-row>"
);
assert_eq!(enforce_button_fit(&body), body);
}
#[test]
fn test_rows_and_trailer_start_a_fresh_markdown_block() {
let options: Vec<String> = vec!["One".into(), "Two".into(), "Three".into()];
for prose in [false, true] {
let mut md = String::from("Answer paragraph.\nSecond line.");
append_rows_and_trailer_md(&mut md, &options, "tok", prose, Some("Sign-off."));
let rows = suggestion_rows_rich_html(&options, "tok");
let sep = "\n\n<tg-button-row>";
assert!(
md.contains(sep),
"prose={prose}: rows not block-separated: {md}"
);
let trailer_sep = "\n\nSign-off.";
assert!(
md.contains(trailer_sep),
"prose={prose}: trailer not block-separated: {md}"
);
assert!(md.ends_with("Sign-off."));
assert!(md.contains(&rows));
if prose {
assert!(md.contains("1. One"), "numbered fold body line: {md}");
assert!(
!md.contains("Go:") && !md.contains("**"),
"no Go-tier artifacts: {md}"
);
}
}
let mut md = String::from("Answer.\n");
append_rows_and_trailer_md(&mut md, &options, "tok", false, None);
assert!(md.starts_with("Answer.\n\n<tg-button-row>"), "{md}");
assert!(!md.starts_with("Answer.\n\n\n"), "{md}");
}