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()
}
fn styled_opts(v: &[&str]) -> Vec<crate::brain::tools::suggest_options::SuggestionItem> {
v.iter()
.map(|s| crate::brain::tools::suggest_options::SuggestionItem::default_styled(*s))
.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(&styled_opts(&["Ship it", &"x".repeat(30)]), token);
assert!(
html.contains(&format!(
">{}</tg-button>",
go_button_label(1, "Ship it", false)
)) && html.contains(&format!(
">{}</tg-button>",
go_button_label(2, &"x".repeat(30), 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, "Ship it", true), "Go!");
assert_eq!(go_button_label(1, "Deploy to prod", false), "1. Deploy");
assert_eq!(go_button_label(2, "Cancel run", false), "2. Cancel");
let single_html = suggestion_rows_rich_html(&styled_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(&styled_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(">Go!</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_reshapes_over_budget_shared_row_to_column() {
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("<ol>"), "{out}");
assert!(!out.contains(">1</tg-button>"), "{out}");
assert!(out.contains("Полёт норм!!"), "{out}");
assert!(out.contains("Всё чётко!!!"), "{out}");
assert_eq!(
out,
"<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">Полёт норм!!</tg-button></tg-button-row>\n<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:1\">Всё чётко!!!</tg-button></tg-button-row>"
);
assert_eq!(enforce_button_fit(&out), out);
}
#[test]
fn test_enforce_button_fit_folds_multi_button_row_when_label_exceeds_solo_budget() {
let long_label = "Проверка ширины кнопки хххххххххЖЖЖ"; let body = format!(
"<tg-button-row>\
<tg-button type=\"callback_data\" data=\"followup:t:0\">{long_label}</tg-button>\
<tg-button type=\"callback_data\" data=\"followup:t:1\">OK</tg-button>\
</tg-button-row>"
);
let out = enforce_button_fit(&body);
assert!(out.contains(">1. Проверка</tg-button>"), "{out}");
assert!(out.contains(">2. OK</tg-button>"), "{out}");
assert!(out.contains(&format!("<li>{long_label}</li>")), "{out}");
assert!(out.contains("<li>OK</li>"), "{out}");
assert_eq!(enforce_button_fit(&out), out);
}
#[test]
fn test_enforce_button_fit_groups_contiguous_rows_into_single_set() {
let body = "<tg-button-row>\
<tg-button type=\"callback_data\" data=\"followup:t:0\">Option One</tg-button>\
</tg-button-row>\n\n<tg-button-row>\
<tg-button type=\"callback_data\" data=\"followup:t:1\">Option Two Long</tg-button>\
<tg-button type=\"callback_data\" data=\"followup:t:2\">Option Three Long</tg-button>\
</tg-button-row>";
let out = enforce_button_fit(body);
assert!(!out.contains("<ol>"), "{out}");
assert!(!out.contains(">1</tg-button>"), "{out}");
let expected = "<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">Option One</tg-button></tg-button-row>\n<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:1\">Option Two Long</tg-button></tg-button-row>\n<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:2\">Option Three Long</tg-button></tg-button-row>";
assert_eq!(out, expected);
assert_eq!(enforce_button_fit(&out), out);
}
#[test]
fn test_enforce_button_fit_splits_sets_on_intervening_non_whitespace_text() {
let body = "<p>First question:</p>\n\
<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">Yes</tg-button><tg-button type=\"callback_data\" data=\"followup:t:1\">No</tg-button></tg-button-row>\n\
<p>Second question:</p>\n\
<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:2\">Choice Alpha Long</tg-button><tg-button type=\"callback_data\" data=\"followup:t:3\">Choice Beta Long</tg-button></tg-button-row>\n\
<p>Third question:</p>\n\
<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:4\">Проверка ширины кнопки хххххххххЖЖЖ</tg-button></tg-button-row>";
let out = enforce_button_fit(body);
assert!(out.contains("<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">Yes</tg-button><tg-button type=\"callback_data\" data=\"followup:t:1\">No</tg-button></tg-button-row>"), "{out}");
assert!(out.contains("<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:2\">Choice Alpha Long</tg-button></tg-button-row>\n<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:3\">Choice Beta Long</tg-button></tg-button-row>"), "{out}");
assert!(out.contains("<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:4\">Go!</tg-button></tg-button-row>\n<ol><li>Проверка ширины кнопки хххххххххЖЖЖ</li></ol>"), "{out}");
assert!(out.contains("<p>First question:</p>"), "{out}");
assert!(out.contains("<p>Second question:</p>"), "{out}");
assert!(out.contains("<p>Third question:</p>"), "{out}");
assert_eq!(enforce_button_fit(&out), out);
}
#[test]
fn test_enforce_button_fit_handles_malformed_and_empty_rows() {
let body = "<tg-button-row></tg-button-row>\n<tg-button-row><tg-button type=\"callback_data\" data=\"test\">OK</tg-button></tg-button-row>";
let out = enforce_button_fit(body);
assert!(out.contains("OK"), "{out}");
assert_eq!(enforce_button_fit(&out), 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 _raw_options: Vec<String> = vec!["One".into(), "Two".into(), "Three".into()];
let options = styled_opts(&["One", "Two", "Three"]);
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("---\n\n1. One"),
"isolated 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}");
}
#[test]
fn test_owner_label_with_ampersand_survives_the_rich_funnel() {
let label = "Acknowledge & stamp gap closed";
let raw_units = label.chars().count();
let escaped_units = crate::channels::telegram::markdown::escape_html(label)
.chars()
.count();
assert_eq!(raw_units, 30, "the solo budget is 30 display units");
assert_eq!(
escaped_units, 34,
"the escaped form is what the enforcer used to measure"
);
assert!(
escaped_units > SINGLE_BUTTON_MAX_UNITS && raw_units <= SINGLE_BUTTON_MAX_UNITS,
"the overcount tripped the fold: {escaped_units} > {SINGLE_BUTTON_MAX_UNITS} >= {raw_units}"
);
let token = "ab12cd34";
let emitted = suggestion_rows_rich_html(&styled_opts(&[label]), token);
let shipped = enforce_button_fit(&emitted);
assert_eq!(
shipped, emitted,
"a row the emitter approved must ship byte-identical through the funnel"
);
assert!(
shipped.contains("Acknowledge & stamp gap closed"),
"the escaped label rides the button verbatim: {shipped}"
);
assert!(!shipped.contains("<ol>"), "no fold list: {shipped}");
assert!(
!shipped.contains(">1</tg-button>"),
"no bare-digit button: {shipped}"
);
assert!(
shipped.contains(&format!("{FOLLOWUP_PREFIX}{token}:0")),
"callback routing is untouched: {shipped}"
);
}
#[test]
fn test_fold_button_label_decodes_the_first_word_and_re_escapes_it() {
let a_raw = "R&D review the whole roadmap thing"; let a_esc = "R&D review the whole roadmap thing"; let b = "Escalate to the vendor immediately"; assert!(
a_raw.chars().count() > SINGLE_BUTTON_MAX_UNITS
&& b.chars().count() > SINGLE_BUTTON_MAX_UNITS,
"both labels must exceed the solo budget for the set to fold"
);
assert_eq!(
crate::channels::telegram::markdown::escape_html(a_raw),
a_esc,
"the enforcer sees the escaped form"
);
let body = format!(
"<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">{a_esc}\
</tg-button><tg-button type=\"callback_data\" data=\"followup:t:1\">{b}\
</tg-button></tg-button-row>"
);
let out = enforce_button_fit(&body);
assert!(
out.contains(">1. R&D</tg-button>"),
"the fold label is the decoded first word, re-escaped once: {out}"
);
assert!(out.contains(">2. Escalate</tg-button>"), "{out}");
assert!(
!out.contains("R&amp;D"),
"the fold label must not be double-escaped: {out}"
);
assert!(
!out.contains("1. R&D review"),
"the fold label is the first word only, not the whole label: {out}"
);
assert!(out.contains(&format!("<li>{a_esc}</li>")), "{out}");
assert!(out.contains(&format!("<li>{b}</li>")), "{out}");
assert_eq!(enforce_button_fit(&out), out, "idempotent");
}
#[test]
fn test_fold_falls_back_to_the_bare_index_when_the_first_word_overflows() {
let long_first = format!("{} tail", "x".repeat(28)); let other = "Escalate to the vendor immediately"; let naive = go_button_label(1, &long_first, false);
assert_eq!(naive.chars().count(), 31, "`1. ` + 28 units");
assert!(
!row_fits(&[naive.as_str()]),
"the naive fold label overflows the solo budget"
);
let fallback = go_button_label(1, "", false);
assert!(
row_fits(&[fallback.as_str()]),
"the bare-index fallback always fits"
);
let body = format!(
"<tg-button-row><tg-button type=\"callback_data\" data=\"followup:t:0\">{long_first}\
</tg-button><tg-button type=\"callback_data\" data=\"followup:t:1\">{other}\
</tg-button></tg-button-row>"
);
let out = enforce_button_fit(&body);
assert!(
out.contains(">1</tg-button>"),
"an over-long first word falls back to the bare index: {out}"
);
assert!(
!out.contains(">1. x"),
"the overflowing tier label is not written: {out}"
);
assert!(
out.contains(">2. Escalate</tg-button>"),
"the sibling button keeps its tier label: {out}"
);
assert!(out.contains(&format!("<li>{long_first}</li>")), "{out}");
assert_eq!(enforce_button_fit(&out), out, "idempotent");
}