use crate::channels::telegram::flow::{
FlowEntry, FlowHeader, FlowOutcome, HeaderMarkup, extract_status_from_text, flow_header_text,
humanize_duration, latest_activity_preview, pop_trailing_folded_texts,
render_flow_details_with, render_flow_html_with,
};
use crate::channels::telegram::handler::{
FlowLine, folded_duplicates_final, render_flow_details, render_flow_html, render_flow_rich,
};
fn tline(label: &str, context: &str) -> FlowLine {
FlowLine::Tool {
label: label.to_string(),
context: context.to_string(),
raw_context: String::new(),
}
}
fn bash_line(raw_command: &str) -> FlowLine {
let input = serde_json::json!({ "command": raw_command });
FlowLine::Tool {
label: "⚙️ bash".to_string(),
context: crate::utils::tool_context_hint("bash", &input),
raw_context: crate::utils::tool_status_source("bash", &input),
}
}
#[test]
fn empty_group_renders_footer_only() {
assert_eq!(render_flow_html(&[], None), "⏱ 0:00");
assert_eq!(render_flow_html(&[], Some("45s")), "⏱ 0:00");
}
#[test]
fn single_tool_renders_block() {
let out = render_flow_html(&[tline("✅ bash", "git status")], None);
assert_eq!(
out,
"<blockquote expandable><b>✅ bash</b> <code>git status</code></blockquote>\n\
⚙️ ✅ bash git status • 1 tool calls • ⏱ 0:00"
);
assert!(out.contains("<blockquote expandable>"));
}
#[test]
fn single_tool_without_context_omits_trailing_space() {
let out = render_flow_html(&[tline("⚙️ web_search", "")], None);
assert!(out.contains("<blockquote expandable><b>⚙️ web_search</b></blockquote>"));
assert!(!out.contains("<b>⚙️ web_search</b> "));
assert!(!out.contains("<code>"));
}
#[test]
fn multiple_tools_render_expandable_blockquote() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
tline("✅ read_file", "handler.rs"),
tline("❌ grep", "pattern"),
],
None,
);
assert!(out.starts_with("<blockquote expandable><b>✅ bash</b> <code>cargo fmt</code>\n\n"));
assert!(out.contains("</blockquote>\n"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ ❌ grep pattern • 3 tool calls • ⏱ 0:00");
assert!(out.contains("<b>✅ bash</b> <code>cargo fmt</code>"));
assert!(out.contains("<b>✅ read_file</b> <code>handler.rs</code>"));
assert!(out.contains("<b>❌ grep</b> <code>pattern</code>"));
}
#[test]
fn blocks_are_separated_by_blank_lines() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
FlowLine::Text("Reformatted three files.".to_string()),
tline("✅ read_file", "handler.rs"),
],
None,
);
assert!(out.starts_with("<blockquote expandable><b>✅ bash</b> <code>cargo fmt</code>\n\n"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(
footer,
"⚙️ Reformatted three files. • 2 tool calls • ⏱ 0:00"
);
assert!(out.contains("<b>✅ bash</b> <code>cargo fmt</code>\n\nReformatted three files."));
assert!(
out.contains("Reformatted three files.\n\n<b>✅ read_file</b> <code>handler.rs</code>")
);
}
#[test]
fn tool_context_renders_as_monospace() {
let out = render_flow_html(
&[
tline("✅ read", "src/channels/telegram/handler.rs"),
tline("✅ bash", "cargo clippy --all-features"),
],
None,
);
assert!(out.contains("<b>✅ read</b> <code>src/channels/telegram/handler.rs</code>"));
assert!(out.contains("<b>✅ bash</b> <code>cargo clippy --all-features</code>"));
}
#[test]
fn intermediate_text_renders_inline_markdown() {
let out = render_flow_html(
&[
tline("✅ bash", "grep foo"),
FlowLine::Text("Calling `analyze_image` then **committing** the *fix*.".to_string()),
],
None,
);
assert!(out.contains("<code>analyze_image</code>"));
assert!(out.contains("<b>committing</b>"));
assert!(out.contains("<i>fix</i>"));
assert!(!out.contains("`analyze_image`"));
assert!(!out.contains("**committing**"));
}
#[test]
fn never_emits_details_tags() {
let out = render_flow_html(&[tline("✅ a", "x"), tline("✅ b", "y")], None);
assert!(!out.contains("<details>"));
assert!(!out.contains("<summary>"));
}
#[test]
fn escapes_html_in_labels_and_context() {
let out = render_flow_html(
&[
tline("✅ bash", "grep '<details>' & \"stuff\""),
tline("✅ edit_file", "a < b > c"),
],
None,
);
assert!(out.contains("grep '<details>' & \"stuff\""));
assert!(out.contains("a < b > c"));
assert!(!out.contains("'<details>'"));
}
#[test]
fn preview_uses_whole_human_readable_text_untruncated() {
let long = "First paragraph of the plan.\nSecond line with more detail.\nThird line that pushes well past the old ninety-six character truncation limit so the whole thing survives.";
let out = latest_activity_preview(&[
tline("✅ bash", "cargo test"),
FlowLine::Text(long.to_string()),
]);
assert_eq!(out.as_deref(), Some(long));
}
#[test]
fn preview_skips_json_last_entry_back_to_narration() {
let out = latest_activity_preview(&[
FlowLine::Text("Checking the model roster.".to_string()),
FlowLine::Text("{\"model\": \"deepseek-v4-flash\", \"ok\": true}".to_string()),
]);
assert_eq!(out.as_deref(), Some("Checking the model roster."));
}
#[test]
fn preview_skips_code_block_last_entry() {
let out = latest_activity_preview(&[
FlowLine::Text("Here is the fix.".to_string()),
FlowLine::Text("```rust\nfn main() {}\n```".to_string()),
]);
assert_eq!(out.as_deref(), Some("Here is the fix."));
}
#[test]
fn preview_strips_inline_markdown_markers() {
let out = latest_activity_preview(&[FlowLine::Text(
"Calling `grep` then **committing** the *fix*.".to_string(),
)]);
assert_eq!(
out.as_deref(),
Some("Calling grep then committing the fix.")
);
}
#[test]
fn preview_keeps_one_word_sentence_but_skips_bare_path() {
assert_eq!(
latest_activity_preview(&[FlowLine::Text("Done.".to_string())]).as_deref(),
Some("Done.")
);
let out = latest_activity_preview(&[
tline("✅ read_file", "handler.rs"),
FlowLine::Text("src/channels/telegram/flow.rs".to_string()),
]);
assert_eq!(out.as_deref(), Some("✅ read_file handler.rs"));
}
#[test]
fn preview_falls_back_to_tool_when_no_human_readable_text() {
let out = latest_activity_preview(&[
tline("✅ read_file", "handler.rs"),
FlowLine::Text("[1,2,3]".to_string()),
]);
assert_eq!(out.as_deref(), Some("✅ read_file handler.rs"));
}
#[test]
fn preview_is_none_when_empty() {
assert_eq!(latest_activity_preview(&[]), None);
}
#[test]
fn bash_comment_single_strips_decoration() {
assert_eq!(
extract_status_from_text("# --- Setup environment ---\nexport FOO=bar").as_deref(),
Some("Setup environment")
);
}
#[test]
fn bash_comment_multiple_join_with_newlines() {
let cmd = "# Step one\napt-get update\n# Step two\napt-get install foo";
assert_eq!(
extract_status_from_text(cmd).as_deref(),
Some("Step one\nStep two")
);
}
#[test]
fn bash_comment_ignores_inline_hash_and_shebang() {
let cmd = "#!/bin/bash\ncurl https://x.com/a#frag\necho ok";
assert_eq!(extract_status_from_text(cmd), None);
}
#[test]
fn bash_comment_none_when_no_comments() {
assert_eq!(extract_status_from_text("cargo build --release"), None);
}
#[test]
fn preview_uses_bash_comments_when_no_narration() {
let out = latest_activity_preview(&[bash_line("# --- Installing deps ---\nnpm install")]);
assert_eq!(out.as_deref(), Some("Installing deps"));
}
#[test]
fn preview_bash_comment_on_first_line_survives_decoration() {
let out = latest_activity_preview(&[bash_line("# --- Setup environment ---\nexport FOO=bar")]);
assert_eq!(out.as_deref(), Some("Setup environment"));
}
#[test]
fn preview_bash_comment_survives_long_command_no_truncation() {
let long = format!(
"# Build step\n{}\n# Deploy step\nmake deploy",
"echo padding-".repeat(12)
);
assert!(long.len() > 80);
let out = latest_activity_preview(&[bash_line(&long)]);
assert_eq!(out.as_deref(), Some("Build step\nDeploy step"));
}
#[test]
fn preview_narration_beats_bash_comments() {
let out = latest_activity_preview(&[
bash_line("# --- Installing deps ---\nnpm install"),
FlowLine::Text("Wiring up the new module.".to_string()),
]);
assert_eq!(out.as_deref(), Some("Wiring up the new module."));
}
#[test]
fn tool_plus_text_folds_into_one_blockquote() {
let out = render_flow_html(
&[
tline("✅ bash", "git status"),
FlowLine::Text("Checked the tree, all clean.".to_string()),
tline("✅ read_file", "handler.rs"),
],
None,
);
assert!(out.starts_with("<blockquote expandable><b>✅ bash</b> <code>git status</code>\n\n"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(
footer,
"⚙️ Checked the tree, all clean. • 2 tool calls • ⏱ 0:00"
);
assert!(out.contains("<b>✅ bash</b> <code>git status</code>"));
assert!(out.contains("Checked the tree, all clean."));
assert!(out.contains("<b>✅ read_file</b> <code>handler.rs</code>"));
assert!(!out.contains("<details>"));
}
#[test]
fn text_only_flow_uses_processing_log_header() {
let out = render_flow_html(&[FlowLine::Text("Switching provider…".to_string())], None);
assert!(out.contains("<blockquote expandable>Switching provider…</blockquote>"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ Switching provider… • ⏱ 0:00");
assert!(!out.contains("tool calls"));
}
#[test]
fn intermediate_text_is_html_escaped_in_flow() {
let out = render_flow_html(
&[
tline("✅ bash", "echo hi"),
FlowLine::Text("result: <b>bold</b> & <script>alert(1)</script>".to_string()),
],
None,
);
assert!(out.contains("<b>bold</b> & <script>"));
assert!(!out.contains("<script>"));
}
#[test]
fn blank_text_entries_are_dropped() {
let out = render_flow_html(
&[tline("✅ bash", "x"), FlowLine::Text(" ".to_string())],
None,
);
assert_eq!(
out,
"<blockquote expandable><b>✅ bash</b> <code>x</code></blockquote>\n\
⚙️ ✅ bash x • 1 tool calls • ⏱ 0:00"
);
}
#[test]
fn empty_flow_renders_footer_only() {
assert_eq!(render_flow_html(&[], None), "⏱ 0:00");
}
#[test]
fn folded_dup_matches_exact_final() {
let answer = "The rebuild finished and the binary is swapped in.";
assert!(folded_duplicates_final(answer, answer));
}
#[test]
fn folded_dup_matches_truncated_prefix() {
let folded = "Yes, Adolfo. After you told me to search for the tool, I";
let final_text =
"Yes, Adolfo. After you told me to search for the tool, I found it and used it.";
assert!(folded_duplicates_final(folded, final_text));
}
#[test]
fn folded_dup_ignores_whitespace_differences() {
let folded = "line one\n\n line two three four five";
let final_text = "line one line two three four five and the rest of the answer here";
assert!(folded_duplicates_final(folded, final_text));
}
#[test]
fn folded_dup_rejects_distinct_narration() {
let folded = "Let me trace the delivery path first.";
let final_text = "The root cause is an exact-equality dedup that misses a truncated prefix.";
assert!(!folded_duplicates_final(folded, final_text));
}
#[test]
fn folded_dup_rejects_short_shared_opening() {
let folded = "Done.";
let final_text = "Done. Here is the full breakdown of everything that changed this turn.";
assert!(!folded_duplicates_final(folded, final_text));
}
#[test]
fn folded_dup_exact_short_answer_matches() {
assert!(folded_duplicates_final("Dropped it.", "Dropped it."));
}
#[test]
fn folded_dup_exact_short_with_whitespace_matches() {
assert!(folded_duplicates_final(" Dropped it.\n", "Dropped it."));
}
#[test]
fn folded_dup_empty_sides_never_match() {
assert!(!folded_duplicates_final("", ""));
assert!(!folded_duplicates_final("", "Dropped it."));
assert!(!folded_duplicates_final("Dropped it.", ""));
}
#[test]
fn live_status_rides_in_footer() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
FlowLine::Text("Reading the handler.".to_string()),
tline("⚙️ read_file", "handler.rs"),
],
Some("45s"),
);
assert!(out.starts_with("<blockquote expandable><b>✅ bash</b> <code>cargo fmt</code>\n\n"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ Reading the handler. • 2 tool calls • ⏱ 0:00");
}
#[test]
fn no_duration_still_leads_with_activity() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
tline("✅ read_file", "handler.rs"),
],
None,
);
assert!(out.starts_with("<blockquote expandable><b>✅ bash</b> <code>cargo fmt</code>\n\n"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ ✅ read_file handler.rs • 2 tool calls • ⏱ 0:00");
assert!(!out.contains("45s"));
}
#[test]
fn running_tool_fallback_does_not_double_the_gear() {
let out = render_flow_html(
&[
tline("⚙️ bash", "gh repo view"),
tline("⚙️ bash", "gh pr list"),
],
Some("10s"),
);
assert!(
!out.contains("⚙️ ⚙️"),
"the running-tool fallback must not double the footer gear: {out}"
);
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ bash gh pr list • 2 tool calls • ⏱ 0:00");
}
#[test]
fn live_status_on_text_only_flow_uses_processing_log_header() {
let out = render_flow_html(
&[FlowLine::Text("Looking into it.".to_string())],
Some("15s"),
);
assert!(out.contains("<blockquote expandable>Looking into it.</blockquote>"));
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "⚙️ Looking into it. • ⏱ 0:00");
}
#[test]
fn single_tool_progress_rides_in_footer() {
let out = render_flow_html(&[tline("⚙️ bash", "git status")], Some("bash • 5s"));
assert_eq!(
out,
"<blockquote expandable><b>⚙️ bash</b> <code>git status</code></blockquote>\n\
⚙️ bash git status • 1 tool calls • ⏱ 0:00"
);
assert!(out.contains("<blockquote expandable>"));
}
#[test]
fn humanize_duration_precise_then_minutes() {
assert_eq!(humanize_duration(0), "0s");
assert_eq!(humanize_duration(45), "45s");
assert_eq!(humanize_duration(59), "59s");
assert_eq!(humanize_duration(60), "1 min 0s");
assert_eq!(humanize_duration(90), "1 min 30s");
assert_eq!(humanize_duration(300), "5 min 0s");
assert_eq!(humanize_duration(3661), "61 min 1s");
}
#[test]
fn flow_header_live_and_settled_formats() {
assert_eq!(
flow_header_text(
3,
&FlowHeader::Live(Some("45s")),
Some("Reading logs"),
HeaderMarkup::Html
),
"⚙️ <b>Reading logs</b> • <i>3 tool calls</i> • <i>45s</i>"
);
assert_eq!(
flow_header_text(3, &FlowHeader::Live(Some("45s")), None, HeaderMarkup::Html),
"⚙️ <i>3 tool calls</i> • <i>45s</i>"
);
assert_eq!(
flow_header_text(3, &FlowHeader::Live(None), None, HeaderMarkup::Html),
"<b>3 tool calls</b>"
);
assert_eq!(
flow_header_text(0, &FlowHeader::Live(None), None, HeaderMarkup::Html),
"<b>Processing log</b>"
);
assert_eq!(
flow_header_text(
3,
&FlowHeader::Live(Some("45s")),
Some("Reading logs"),
HeaderMarkup::Markdown
),
"⚙️ **Reading logs** • _3 tool calls_ • _45s_"
);
assert_eq!(
flow_header_text(
12,
&FlowHeader::Settled {
icon: "✅",
verb: "Finished",
duration: "45s"
},
None,
HeaderMarkup::Html
),
"<b>✅ Finished (12 tool calls, 45s)</b>"
);
assert_eq!(
flow_header_text(
0,
&FlowHeader::Settled {
icon: "✅",
verb: "Finished",
duration: "45s"
},
None,
HeaderMarkup::Html
),
"<b>✅ Finished (45s)</b>"
);
}
#[test]
fn flow_outcome_icons_and_verbs() {
assert_eq!(FlowOutcome::Finished.icon_verb(), ("✅", "Finished"));
assert_eq!(FlowOutcome::Failed.icon_verb(), ("❌", "Failed"));
assert_eq!(FlowOutcome::TimedOut.icon_verb(), ("⏱", "Timed out"));
}
#[test]
fn settled_outcome_renders_block_header_over_lone_tool() {
let out = render_flow_html_with(
&[tline("✅ bash", "cargo test")],
&FlowHeader::Settled {
icon: "❌",
verb: "Failed",
duration: "12s",
},
);
assert!(
out.starts_with(
"<blockquote expandable><b>✅ bash</b> <code>cargo test</code></blockquote>"
)
);
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(
footer, "❌ Failed • 1 tool calls • ⏱ 0:00",
"settled footer: {out}"
);
}
#[test]
fn settled_block_carries_no_activity_preview_classic() {
let out = render_flow_html_with(
&[
tline("✅ bash", "cargo test"),
FlowLine::Text("Running the test suite".to_string()),
],
&FlowHeader::Settled {
icon: "✅",
verb: "Finished",
duration: "3 min 15s",
},
);
let footer = out.rsplit('\n').next().unwrap();
assert_eq!(footer, "✅ Finished • 1 tool calls • ⏱ 0:00");
assert!(
!footer.contains("Running the test suite"),
"settled footer must not carry a preview: {out}"
);
assert!(
!footer.contains("⚙️"),
"settled footer carries no cog: {out}"
);
assert!(out.contains("Running the test suite"));
}
#[test]
fn settled_block_carries_no_activity_preview_rich() {
let out = render_flow_details_with(
&[
tline("✅ bash", "cargo test"),
FlowLine::Text("Running the test suite".to_string()),
],
&FlowHeader::Settled {
icon: "✅",
verb: "Finished",
duration: "3 min 15s",
},
);
let summary_end = out.find("</summary>").expect("summary");
let summary = &out[..summary_end];
assert!(summary.contains("✅ Finished • 1 tool calls"));
assert!(
!summary.contains("Running the test suite"),
"settled summary must not carry a preview: {summary}"
);
assert!(
!summary.contains("⚙️"),
"settled summary carries no cog: {summary}"
);
}
#[test]
fn rich_empty_group_renders_header_only() {
assert_eq!(render_flow_rich(&[], None), "**Processing log**");
}
#[test]
fn rich_single_tool_renders_plain_line() {
let out = render_flow_rich(&[tline("✅ bash", "git status")], None);
assert_eq!(out, "**✅ bash** `git status`");
assert!(!out.contains(">"));
}
#[test]
fn rich_multiple_tools_render_markdown_header() {
let out = render_flow_rich(
&[tline("✅ bash", "git status"), tline("✅ read", "file.rs")],
None,
);
assert!(out.starts_with("⚙️ **✅ read file.rs** • _2 tool calls_\n\n"));
assert!(out.contains("**✅ bash** `git status`"));
assert!(out.contains("**✅ read** `file.rs`"));
}
#[test]
fn rich_live_status_in_header() {
let out = render_flow_rich(
&[
tline("✅ bash", "x"),
FlowLine::Text("Searching.".to_string()),
tline("⚙️ grep", "pattern"),
],
Some("10s"),
);
assert!(out.starts_with("⚙️ **Searching.** • _2 tool calls_ • _10s_\n\n"));
}
#[test]
fn rich_single_tool_live_status_appends() {
let out = render_flow_rich(&[tline("⚙️ bash", "git status")], Some("bash • 5s"));
assert_eq!(out, "**⚙️ bash** `git status` • bash • 5s");
}
#[test]
fn details_empty_group_renders_footer_only() {
assert_eq!(render_flow_details(&[], None), "<sub>⏱ 0:00</sub>");
}
#[test]
fn details_single_tool_renders_details_block() {
let out = render_flow_details(&[tline("✅ bash", "git status")], None);
assert_eq!(
out,
"<details><summary><sub>⚙️ ✅ bash git status • 1 tool calls • ⏱ 0:00</sub></summary>\
<p><b>✅ bash</b> <code>git status</code></p></details>"
);
assert!(out.contains("<details>"));
}
#[test]
fn details_multiple_tools_wrap_in_collapsed_details() {
let out = render_flow_details(
&[tline("✅ bash", "git status"), tline("✅ read", "file.rs")],
None,
);
assert!(out.starts_with(
"<details><summary><sub>⚙️ ✅ read file.rs • 2 tool calls • ⏱ 0:00</sub></summary>"
));
assert!(out.ends_with("</details>"));
assert!(!out.contains("<details open"));
assert!(out.contains("<p><b>✅ bash</b> <code>git status</code></p>"));
assert!(out.contains("<p><b>✅ read</b> <code>file.rs</code></p>"));
}
#[test]
fn details_summary_carries_live_status() {
let out = render_flow_details(
&[
tline("✅ bash", "x"),
FlowLine::Text("Grepping.".to_string()),
tline("⚙️ grep", "pattern"),
],
Some("10s"),
);
let summary_end = out.find("</summary>").expect("summary");
let summary = &out[..summary_end];
assert!(summary.contains("⚙️ Grepping. • 2 tool calls • ⏱ 0:00"));
assert!(summary.contains("<sub>⚙️ Grepping."));
assert!(summary.contains("Grepping."));
}
#[test]
fn details_collapsed_summary_shows_intermediate_narration() {
let out = render_flow_details(
&[
FlowLine::Text("Running the test suite".to_string()),
tline("⚙️ bash", "cargo test"),
],
Some("45s"),
);
let summary_end = out.find("</summary>").expect("summary");
let summary = &out[..summary_end];
assert!(summary.contains("⚙️ Running the test suite"));
}
#[test]
fn details_escapes_html_in_tool_context() {
let out = render_flow_details(&[tline("✅ bash", "a < b"), tline("✅ read", "x.rs")], None);
assert!(out.contains("<code>a < b</code>"));
}
#[test]
fn collapsed_preview_prefers_narration_over_tool_line() {
let out = render_flow_html(
&[
FlowLine::Text("Checking how the scheduler resolves the next run".to_string()),
tline("✅ bash", "grep flow"),
tline("⚙️ read_file", "src/agent.rs"),
],
None,
);
let footer = out.rsplit('\n').next().unwrap();
assert!(
footer.contains("Checking how the scheduler resolves the next run"),
"footer must carry the narration preview: {footer}"
);
assert!(out.contains("<b>⚙️ read_file</b> <code>src/agent.rs</code>"));
}
#[test]
fn preview_keeps_long_text_whole_and_strips_markdown() {
let long = format!("**{}**", "x".repeat(200));
let out = render_flow_html(&[tline("✅ bash", "a"), FlowLine::Text(long)], None);
let footer = out.rsplit('\n').next().unwrap();
assert!(!footer.contains('…'), "not truncated: {footer}");
assert!(
footer.contains(&"x".repeat(200)),
"whole text kept: {footer}"
);
assert!(!footer.contains("**"), "markers stripped: {footer}");
}
#[test]
fn trailing_text_run_pops_joined_in_order() {
let mut entries = vec![
FlowEntry::Tool(0),
FlowEntry::Text("first part of the answer".into()),
FlowEntry::Text("Already factored in. Standing by.".into()),
];
let reclaimed = pop_trailing_folded_texts(&mut entries).expect("reclaims");
assert_eq!(
reclaimed,
"first part of the answer\n\nAlready factored in. Standing by."
);
assert_eq!(entries.len(), 1, "tool entry stays");
assert!(matches!(entries[0], FlowEntry::Tool(0)));
}
#[test]
fn text_only_flow_pops_everything() {
let mut entries = vec![
FlowEntry::Text("the whole answer".into()),
FlowEntry::Text("plus a follow-up".into()),
];
let reclaimed = pop_trailing_folded_texts(&mut entries).expect("reclaims");
assert!(reclaimed.starts_with("the whole answer"));
assert!(entries.is_empty());
}
#[test]
fn tool_last_flow_reclaims_nothing() {
let mut entries = vec![FlowEntry::Text("narration".into()), FlowEntry::Tool(0)];
assert!(pop_trailing_folded_texts(&mut entries).is_none());
assert_eq!(entries.len(), 2, "nothing consumed");
}
#[test]
fn long_folded_narration_is_capped_in_the_block() {
let long = "x".repeat(1200);
let out = render_flow_html(
&[
tline("✅ bash", "cargo test"),
FlowLine::Text(long.clone()),
FlowLine::Text("all done here now".into()),
tline("✅ read", "flow.rs"),
],
None,
);
assert!(out.contains('…'), "capped body entry ends with an ellipsis");
assert!(!out.contains(&long), "body narration must be truncated");
}