use crate::channels::telegram::handler::{
FlowLine, folded_duplicates_final, humanize_elapsed, render_flow_html,
};
fn tline(label: &str, context: &str) -> FlowLine {
FlowLine::Tool {
label: label.to_string(),
context: context.to_string(),
}
}
#[test]
fn empty_group_renders_nothing() {
assert_eq!(render_flow_html(&[], None), "");
}
#[test]
fn single_tool_renders_plain_line_without_blockquote() {
let out = render_flow_html(&[tline("✅ bash", "git status")], None);
assert_eq!(out, "<b>✅ bash</b> <code>git status</code>");
assert!(!out.contains("<blockquote"));
}
#[test]
fn single_tool_without_context_omits_trailing_space() {
let out = render_flow_html(&[tline("⚙️ web_search", "")], None);
assert_eq!(out, "<b>⚙️ web_search</b>");
}
#[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>3 tool calls</b>\n"));
assert!(out.ends_with("</blockquote>"));
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>2 tool calls</b>\n\n"));
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 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>2 tool calls</b>\n"));
assert!(out.ends_with("</blockquote>"));
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.starts_with("<blockquote expandable><b>Processing log</b>\n"));
assert!(out.contains("Switching provider…"));
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, "<b>✅ bash</b> <code>x</code>");
}
#[test]
fn empty_flow_renders_nothing() {
assert_eq!(render_flow_html(&[], None), "");
}
#[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_blockquote_header() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
tline("⚙️ read_file", "handler.rs"),
],
Some("read_file · 45s"),
);
assert!(out.starts_with("<blockquote expandable><b>⚙️ 2 tool calls · read_file · 45s</b>\n"));
assert!(out.ends_with("</blockquote>"));
}
#[test]
fn no_status_renders_plain_settled_header() {
let out = render_flow_html(
&[
tline("✅ bash", "cargo fmt"),
tline("✅ read_file", "handler.rs"),
],
None,
);
assert!(out.starts_with("<blockquote expandable><b>2 tool calls</b>\n"));
assert!(!out.contains("⚙️ 2 tool calls"));
}
#[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.starts_with("<blockquote expandable><b>⚙️ Processing log · 15s</b>\n"));
}
#[test]
fn live_status_appends_to_single_tool_line() {
let out = render_flow_html(&[tline("⚙️ bash", "git status")], Some("bash · 5s"));
assert_eq!(out, "<b>⚙️ bash</b> <code>git status</code> · bash · 5s");
assert!(!out.contains("<blockquote"));
}
#[test]
fn humanize_elapsed_snaps_to_five_second_steps() {
assert_eq!(humanize_elapsed(0), "0s");
assert_eq!(humanize_elapsed(4), "0s");
assert_eq!(humanize_elapsed(7), "5s");
assert_eq!(humanize_elapsed(59), "55s");
assert_eq!(humanize_elapsed(61), "1m 0s");
assert_eq!(humanize_elapsed(93), "1m 30s");
assert_eq!(humanize_elapsed(3601), "60m 0s");
}