use crate::channels::slack::tool_group::{GroupEntry, GroupState, render};
use slack_morphism::prelude::{SlackChannelId, SlackMessageContent, SlackTs};
fn tool(name: &str, status: Option<bool>) -> GroupEntry {
GroupEntry::Tool {
name: name.to_string(),
context: String::new(),
status,
}
}
fn group(entries: Vec<GroupEntry>, expanded: bool) -> GroupState {
GroupState {
channel: SlackChannelId::new("C1".into()),
entries,
expanded,
}
}
fn text_of(content: &SlackMessageContent) -> String {
content.text.clone().unwrap_or_default()
}
const NARRATION: &str = "Let me verify one thing properly before I report it";
#[test]
fn collapsed_group_hides_narration() {
let g = group(
vec![
tool("bash", Some(true)),
GroupEntry::Note(NARRATION.to_string()),
tool("read_file", Some(true)),
],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(
!text.contains(NARRATION),
"collapsed group must not show narration. Got:\n{text}"
);
}
#[test]
fn expanded_group_shows_narration_in_order() {
let g = group(
vec![
tool("bash", Some(true)),
GroupEntry::Note(NARRATION.to_string()),
tool("read_file", Some(true)),
],
true,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains(NARRATION), "expanded must show it: {text}");
let note_at = text.find(NARRATION).expect("narration present");
let bash_at = text.find("bash").expect("bash present");
let read_at = text.find("read_file").expect("read_file present");
assert!(
bash_at < note_at && note_at < read_at,
"steps must render in the order they happened. Got:\n{text}"
);
}
#[test]
fn summary_counts_steps_and_tools_separately_when_they_differ() {
let g = group(
vec![
tool("bash", Some(true)),
GroupEntry::Note("thinking".to_string()),
GroupEntry::Note("more thinking".to_string()),
tool("read_file", Some(true)),
],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains("4 steps"), "got: {text}");
assert!(text.contains("2 tool calls"), "got: {text}");
}
#[test]
fn summary_stays_tool_only_when_there_is_no_narration() {
let g = group(
vec![tool("bash", Some(true)), tool("read_file", Some(true))],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains("2 tool calls"), "got: {text}");
assert!(
!text.contains("step"),
"no narration means no step count to explain. Got:\n{text}"
);
}
#[test]
fn narration_does_not_count_as_a_running_step() {
let g = group(
vec![
tool("bash", Some(true)),
GroupEntry::Note(NARRATION.to_string()),
],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(
!text.contains("running"),
"a finished turn must not report a running step. Got:\n{text}"
);
assert!(text.contains("✅"), "got: {text}");
}
#[test]
fn a_running_tool_is_still_reported_alongside_narration() {
let g = group(
vec![GroupEntry::Note(NARRATION.to_string()), tool("bash", None)],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains("1 running"), "got: {text}");
}
#[test]
fn a_failed_tool_is_still_reported_alongside_narration() {
let g = group(
vec![
GroupEntry::Note(NARRATION.to_string()),
tool("bash", Some(false)),
],
false,
);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains("1 failed"), "got: {text}");
assert!(text.contains("❌"), "got: {text}");
}
#[test]
fn a_lone_narration_step_renders_without_a_summary_header() {
let g = group(vec![GroupEntry::Note(NARRATION.to_string())], false);
let text = text_of(&render(&g, &SlackTs::new("1.0".into())));
assert!(text.contains(NARRATION), "got: {text}");
}
use crate::channels::slack::tool_group::notes_text;
#[test]
fn a_group_with_no_narration_has_nothing_to_salvage() {
let entries = vec![tool("bash", Some(true)), tool("read_file", Some(true))];
assert_eq!(notes_text(&entries), None);
}
#[test]
fn an_empty_group_has_nothing_to_salvage() {
assert_eq!(notes_text(&[]), None);
}
#[test]
fn narration_is_recovered_in_order() {
let entries = vec![
GroupEntry::Note("First finding.".to_string()),
tool("bash", Some(true)),
GroupEntry::Note("Second finding.".to_string()),
];
let salvaged = notes_text(&entries).expect("two notes are present");
let first = salvaged.find("First finding.").expect("first present");
let second = salvaged.find("Second finding.").expect("second present");
assert!(first < second, "order must hold:\n{salvaged}");
assert!(
!salvaged.contains("bash"),
"tool rows must not leak into the answer:\n{salvaged}"
);
}
#[test]
fn blank_notes_do_not_produce_an_empty_answer() {
let entries = vec![
GroupEntry::Note(" ".to_string()),
GroupEntry::Note("\n".to_string()),
];
assert_eq!(notes_text(&entries), None);
}