use crate::channels::telegram::flow::{
FlowEntry, MAX_TRAILER_CHARS, pop_trailing_folded_texts, settle_options_reclaim,
trailer_promotes_to_answer,
};
#[test]
fn options_pending_reclaims_answer_before_trailing_tool() {
let mut entries = vec![
FlowEntry::Text("the substantive answer".into()),
FlowEntry::Tool(4),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host.as_deref(), Some("the substantive answer"));
assert_eq!(trailer, None, "no post-halt run folded");
assert_eq!(entries.len(), 1, "tool entry restored after reclaim");
assert!(matches!(entries[0], FlowEntry::Tool(4)));
}
#[test]
fn options_pending_pops_whole_run_before_tool() {
let mut entries = vec![
FlowEntry::Text("mid-turn narration".into()),
FlowEntry::Tool(2),
FlowEntry::Text("answer part one".into()),
FlowEntry::Text("answer part two".into()),
FlowEntry::Tool(5),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host.as_deref(), Some("answer part one\n\nanswer part two"));
assert_eq!(trailer, None);
assert_eq!(entries.len(), 3, "narration + old tool + suggest tool stay");
assert!(matches!(entries[0], FlowEntry::Text(_)));
assert!(matches!(entries[1], FlowEntry::Tool(2)));
assert!(matches!(entries[2], FlowEntry::Tool(5)));
}
#[test]
fn options_pending_restores_multiple_trailing_tools_in_order() {
let mut entries = vec![
FlowEntry::Text("answer".into()),
FlowEntry::Tool(1),
FlowEntry::Tool(2),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host.as_deref(), Some("answer"));
assert_eq!(trailer, None);
assert_eq!(entries.len(), 2, "both tools restored");
assert!(matches!(entries[0], FlowEntry::Tool(1)));
assert!(matches!(entries[1], FlowEntry::Tool(2)));
}
#[test]
fn options_pending_without_preceding_text_changes_nothing() {
let mut entries = vec![FlowEntry::Tool(3), FlowEntry::Tool(4)];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host, None);
assert_eq!(trailer, None);
assert_eq!(entries.len(), 2);
assert!(matches!(entries[0], FlowEntry::Tool(3)));
assert!(matches!(entries[1], FlowEntry::Tool(4)));
}
#[test]
fn options_pending_pops_host_and_trailer_around_tool() {
let mut entries = vec![
FlowEntry::Text("mid-turn narration".into()),
FlowEntry::Tool(2),
FlowEntry::Text("the substantive answer".into()),
FlowEntry::Tool(4),
FlowEntry::Text("trailer line one".into()),
FlowEntry::Text("trailer line two".into()),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host.as_deref(), Some("the substantive answer"));
assert_eq!(
trailer.as_deref(),
Some("trailer line one\n\ntrailer line two"),
"post-halt run joins in order"
);
assert_eq!(entries.len(), 3, "narration + old tool + suggest tool stay");
assert!(matches!(entries[0], FlowEntry::Text(_)));
assert!(matches!(entries[1], FlowEntry::Tool(2)));
assert!(matches!(entries[2], FlowEntry::Tool(4)));
}
#[test]
fn options_pending_trailer_only_when_no_host_text() {
let mut entries = vec![
FlowEntry::Tool(4),
FlowEntry::Text("all set — sign-off".into()),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host, None);
assert_eq!(trailer.as_deref(), Some("all set — sign-off"));
assert_eq!(entries.len(), 1);
assert!(matches!(entries[0], FlowEntry::Tool(4)));
}
#[test]
fn options_pending_trailing_text_without_tool_is_host() {
let mut entries = vec![FlowEntry::Text("plain trailing answer".into())];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, true);
assert_eq!(host.as_deref(), Some("plain trailing answer"));
assert_eq!(trailer, None);
assert!(entries.is_empty());
}
#[test]
fn gate_closed_keeps_tool_last_invariant() {
let mut entries = vec![
FlowEntry::Text("the substantive answer".into()),
FlowEntry::Tool(4),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, false);
assert_eq!(host, None);
assert_eq!(trailer, None);
assert_eq!(entries.len(), 2, "nothing consumed");
}
#[test]
fn gate_closed_on_trailing_text_unchanged() {
let mut entries = vec![
FlowEntry::Tool(0),
FlowEntry::Text("plain final answer".into()),
];
let (host, trailer) = pop_trailing_folded_texts(&mut entries, false);
assert_eq!(host.as_deref(), Some("plain final answer"));
assert_eq!(trailer, None);
assert_eq!(entries.len(), 1);
}
#[test]
fn settle_host_wins_content_slot() {
let (text, trailer) = settle_options_reclaim(
"all set, pushed".into(),
Some("the substantive answer".into()),
Some("sign-off paragraph".into()),
);
assert_eq!(text, "the substantive answer");
assert_eq!(trailer.as_deref(), Some("sign-off paragraph"));
}
#[test]
fn settle_drops_trailer_duplicate_of_host() {
let (text, trailer) = settle_options_reclaim(
"ack".into(),
Some("the substantive answer".into()),
Some("the substantive answer".into()),
);
assert_eq!(text, "the substantive answer");
assert_eq!(trailer, None);
}
#[test]
fn settle_content_becomes_trailer_when_no_run_folded() {
let (text, trailer) = settle_options_reclaim(
"unfolded sign-off ack".into(),
Some("the substantive answer".into()),
None,
);
assert_eq!(text, "the substantive answer");
assert_eq!(trailer.as_deref(), Some("unfolded sign-off ack"));
}
#[test]
fn settle_content_duplicate_of_host_not_promoted() {
let (text, trailer) = settle_options_reclaim(
"the substantive answer".into(),
Some("the substantive answer delivered in full".into()),
None,
);
assert_eq!(text, "the substantive answer delivered in full");
assert_eq!(trailer, None);
}
#[test]
fn settle_no_host_uses_content() {
let (text, trailer) =
settle_options_reclaim("the whole answer".into(), None, Some("sign-off".into()));
assert_eq!(text, "the whole answer");
assert_eq!(trailer.as_deref(), Some("sign-off"));
}
#[test]
fn trailer_promotes_to_answer_matrix() {
assert!(trailer_promotes_to_answer(true, 10));
assert!(!trailer_promotes_to_answer(false, MAX_TRAILER_CHARS));
assert!(trailer_promotes_to_answer(false, MAX_TRAILER_CHARS + 1));
}
#[test]
fn settle_no_host_no_trailer_is_stock() {
let (text, trailer) = settle_options_reclaim("the whole answer".into(), None, None);
assert_eq!(text, "the whole answer");
assert_eq!(trailer, None);
}
#[test]
fn settle_oversized_trailer_promoted_to_answer() {
let big_answer = "word ".repeat(121);
assert!(big_answer.chars().count() > MAX_TRAILER_CHARS);
let (text, trailer) = settle_options_reclaim(
"unused content".into(),
Some("Got it — one moment.".into()),
Some(big_answer.clone()),
);
assert_eq!(text, big_answer, "oversized trailer promoted to the answer");
assert_eq!(
trailer.as_deref(),
Some("Got it — one moment."),
"displaced host demotes to trailer"
);
}
#[test]
fn settle_small_trailer_stays_trailer() {
let (text, trailer) = settle_options_reclaim(
"content".into(),
Some("the substantive answer".into()),
Some("all set — sign-off".into()),
);
assert_eq!(text, "the substantive answer");
assert_eq!(trailer.as_deref(), Some("all set — sign-off"));
}
#[test]
fn settle_oversized_trailer_empty_host_drops_demote() {
let big_answer = "word ".repeat(121);
let (text, trailer) = settle_options_reclaim(String::new(), None, Some(big_answer.clone()));
assert_eq!(text, big_answer);
assert_eq!(
trailer, None,
"empty host demotes to None, not an empty trailer"
);
}
#[test]
fn settle_demoted_host_trips_guard_concatenates_into_answer() {
let big_host = "host ".repeat(121);
let big_trailer = "trailer ".repeat(76);
assert!(big_host.chars().count() > MAX_TRAILER_CHARS);
assert!(big_trailer.chars().count() > MAX_TRAILER_CHARS);
let (text, trailer) = settle_options_reclaim(
"unused content".into(),
Some(big_host.clone()),
Some(big_trailer.clone()),
);
assert_eq!(
text,
format!("{big_trailer}\n\n{big_host}"),
"promoted trailer keeps the answer head; the tripping demoted host \
concatenates after it"
);
assert_eq!(
trailer, None,
"trailer slot dies — both texts ride the answer render path"
);
}