use slack_morphism::prelude::SlackChannelId;
use slack_morphism::prelude::SlackTs;
use crate::channels::slack::tool_group::{GroupEntry, GroupState, TurnOutcome, render};
fn group(entries: Vec<GroupEntry>) -> GroupState {
GroupState::new(SlackChannelId::new("C123".into()), entries)
}
fn text_of(g: &GroupState) -> String {
let content = render(g, &SlackTs::new("1.0".into()));
content.text.clone().unwrap_or_default()
}
#[test]
fn activity_prefers_the_latest_narration_note() {
let g = group(vec![
GroupEntry::Tool {
name: "read_file".into(),
context: "src/main.rs".into(),
status: Some(true),
},
GroupEntry::Note("Reading the config first.".into()),
GroupEntry::Tool {
name: "bash".into(),
context: "cargo test".into(),
status: None,
},
GroupEntry::Note("*Now grepping the logs* for the failure.".into()),
]);
let text = text_of(&g);
assert!(
text.contains("Now grepping the logs for the failure."),
"activity text missing: {text}"
);
let act = text.find("Now grepping").expect("activity present");
let counts = text.find("tool call").expect("counts present");
assert!(act < counts, "activity must lead the counts: {text}");
}
#[test]
fn activity_falls_back_to_bash_hash_comments() {
let g = group(vec![
GroupEntry::Tool {
name: "bash".into(),
context: "# Build the thing\ncargo build --release".into(),
status: None,
},
GroupEntry::Tool {
name: "read_file".into(),
context: "src/lib.rs".into(),
status: None,
},
]);
let text = text_of(&g);
assert!(
text.contains("Build the thing"),
"bash # comments missing: {text}"
);
}
#[test]
fn activity_falls_back_to_latest_tool_label() {
let g = group(vec![
GroupEntry::Tool {
name: "read_file".into(),
context: "src/main.rs".into(),
status: Some(true),
},
GroupEntry::Tool {
name: "bash".into(),
context: String::new(),
status: None,
},
]);
let text = text_of(&g);
assert!(text.contains("bash · *2 tool calls*"), "fallback: {text}");
}
#[test]
fn empty_group_keeps_the_bare_counts_shape() {
let g = group(vec![]);
let text = text_of(&g);
assert!(
text.contains("*0 tool calls*"),
"counts missing on empty group: {text}"
);
}
#[test]
fn activity_is_capped_and_raw_output_skipped() {
let long_note = format!(
"{}{}",
"Working through the step list, ".repeat(6),
"done soon"
);
let g = group(vec![
GroupEntry::Note(long_note.clone()),
GroupEntry::Tool {
name: "bash".into(),
context: "src/foo.rs".into(),
status: None,
},
]);
let text = text_of(&g);
assert!(
text.contains("Working through the step list"),
"note lost to raw tool context: {text}"
);
assert!(text.contains('…'), "long activity must be capped: {text}");
let act_len = text
.split("·")
.next()
.map(|s| s.chars().count())
.unwrap_or(0);
assert!(
act_len < 130,
"activity segment too long: {act_len} in {text}"
);
}
#[test]
fn settled_line_never_carries_activity() {
let mut g = group(vec![
GroupEntry::Note("mid-turn narration".into()),
GroupEntry::Tool {
name: "bash".into(),
context: String::new(),
status: Some(true),
},
]);
g.settle(TurnOutcome::Finished, None, Some("ctx".into()));
let text = text_of(&g);
assert!(
!text.contains("mid-turn narration"),
"settled line must stay clean: {text}"
);
assert!(
text.contains("🕒") || text.contains("⏱"),
"settled summary: {text}"
);
}