use std::time::Duration;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn rail_row_columns_identity_activity_and_trailing() {
let row_style = Theme::activity_rail();
let line = RailRow {
connector: tree_connector(true),
identity: vec![
Span::styled("sleep", Theme::text_strong().patch(row_style)),
Span::styled(" ", row_style),
Span::styled("aaaaaaaa", Theme::dim().patch(row_style)),
],
activity: "running".into(),
activity_style: Theme::text(),
trailing: "4s".into(),
trailing_style: Theme::dim(),
row_style,
}
.into_line(80);
let texts: Vec<&str> = line
.spans
.iter()
.map(|span| span.content.as_ref())
.collect();
assert_eq!(texts[0], " └ ");
assert_eq!(texts[1], "sleep");
assert_eq!(texts[2], " ");
assert_eq!(texts[3], "aaaaaaaa");
assert_eq!(texts[4], " · ");
assert_eq!(texts[5], "running");
assert!(texts[6].chars().all(|ch| ch == ' '));
assert_eq!(texts[7], "4s");
}
#[test]
fn rail_row_truncates_when_fixed_width_overflows() {
let row_style = Theme::activity_rail();
let line = RailRow {
connector: tree_connector(true),
identity: vec![Span::styled(
"very-long-command",
Theme::text_strong().patch(row_style),
)],
activity: "running".into(),
activity_style: Theme::text(),
trailing: "12s".into(),
trailing_style: Theme::dim(),
row_style,
}
.into_line(18);
assert_eq!(line.spans.len(), 2);
assert_eq!(line.spans[0].content.as_ref(), " └ ");
let detail = line.spans[1].content.as_ref();
assert!(detail.starts_with("very-long-"));
assert!(display_width(detail) <= 14);
}
#[test]
fn bottom_follow_activity_inset_only_when_activity_and_pinned() {
assert_eq!(bottom_follow_activity_inset(false, true), 0);
assert_eq!(bottom_follow_activity_inset(true, false), 0);
assert_eq!(
bottom_follow_activity_inset(true, true),
ACTIVITY_RAIL_ROWS + ACTIVITY_CONTENT_GAP_ROWS
);
}
#[test]
fn jump_to_bottom_text_reflects_chip_state() {
let binding = "ctrl+e";
assert_eq!(
jump_to_bottom_text(80, binding, false, JumpChipState::Neutral),
"↓ jump to bottom ctrl+e"
);
assert_eq!(
jump_to_bottom_text(80, binding, false, JumpChipState::ResponseReady),
"↓ response ready ctrl+e"
);
assert_eq!(
jump_to_bottom_text(80, binding, false, JumpChipState::ApprovalNeeded),
"↓ approval needed ctrl+e"
);
assert_eq!(
jump_to_bottom_text(80, binding, false, JumpChipState::InputNeeded),
"↓ input needed ctrl+e"
);
}
#[test]
fn jump_to_bottom_attention_states_have_compact_forms() {
let binding = "ctrl+e";
assert_eq!(
jump_to_bottom_text(14, binding, false, JumpChipState::Neutral),
"↓ ctrl+e"
);
assert_eq!(
jump_to_bottom_text(14, binding, false, JumpChipState::ResponseReady),
"↓ ready ctrl+e"
);
assert_eq!(
jump_to_bottom_text(14, binding, false, JumpChipState::ApprovalNeeded),
"↓ ask ctrl+e"
);
assert_eq!(
jump_to_bottom_text(14, binding, false, JumpChipState::InputNeeded),
"↓ input ctrl+e"
);
}
#[test]
fn jump_to_bottom_attention_compact_labels_fit_where_neutral_does() {
let (_, neutral_compact) = JumpChipState::Neutral.labels();
let neutral_width = display_width(neutral_compact);
for state in [
JumpChipState::ResponseReady,
JumpChipState::ApprovalNeeded,
JumpChipState::InputNeeded,
] {
let (full, compact) = state.labels();
assert!(
display_width(compact) <= neutral_width,
"{full:?} compact label {compact:?} is wider than neutral's {neutral_compact:?}"
);
}
}
fn responding_parent() -> (ActivityPhase, Option<ProviderRetryHint>) {
(ActivityPhase::Responding, None)
}
fn counts(subagent_count: usize, job_count: usize) -> BackgroundCounts {
BackgroundCounts {
subagent_count,
job_count,
}
}
#[test]
fn from_parent_and_background_selects_variant() {
let parent = Some(responding_parent());
let (phase, retry) = responding_parent();
assert_eq!(
ActivityStatus::from_parent_and_background(None, counts(0, 0), false),
None
);
assert_eq!(
ActivityStatus::from_parent_and_background(None, counts(0, 0), true),
Some(ActivityStatus::Linger)
);
let cases = [
(
parent,
counts(0, 0),
ActivityStatus::Parent {
phase,
retry,
background: counts(0, 0),
},
),
(
parent,
counts(2, 0),
ActivityStatus::Parent {
phase,
retry,
background: counts(2, 0),
},
),
(
parent,
counts(0, 1),
ActivityStatus::Parent {
phase,
retry,
background: counts(0, 1),
},
),
(
parent,
counts(2, 1),
ActivityStatus::Parent {
phase,
retry,
background: counts(2, 1),
},
),
(None, counts(1, 0), ActivityStatus::Background(counts(1, 0))),
(None, counts(0, 3), ActivityStatus::Background(counts(0, 3))),
(None, counts(2, 1), ActivityStatus::Background(counts(2, 1))),
];
for (parent, background, expected) in cases {
assert_eq!(
ActivityStatus::from_parent_and_background(parent, background, false),
Some(expected),
);
}
}
#[test]
fn activity_label_trails_elapsed_then_drops_it() {
let spinner = LoadingSpinner::FRAMES[0];
let parent = ActivityStatus::Parent {
phase: ActivityPhase::Responding,
retry: None,
background: counts(0, 0),
};
let with_agents = ActivityStatus::Parent {
phase: ActivityPhase::Responding,
retry: None,
background: counts(2, 0),
};
let agents_only = ActivityStatus::Background(counts(2, 0));
let jobs_only = ActivityStatus::Background(counts(0, 1));
let parent_timed = format!("{spinner} responding · 15.0s");
let parent_plain = format!("{spinner} responding");
let agents_timed = format!("{spinner} responding · 2 agents · 15.0s");
let agents_plain = format!("{spinner} responding · 2 agents");
let only_timed = format!("{spinner} 2 agents working · 15.0s");
let jobs_timed = format!("{spinner} 1 job running · 15.0s");
let elapsed = Some(Duration::from_secs(15));
assert_eq!(activity_label(80, parent, elapsed), parent_timed);
assert_eq!(
activity_label(display_width(&parent_plain), parent, elapsed),
parent_plain
);
assert_eq!(activity_label(80, with_agents, elapsed), agents_timed);
assert_eq!(
activity_label(display_width(&agents_plain), with_agents, elapsed),
agents_plain
);
assert_eq!(activity_label(80, agents_only, elapsed), only_timed);
assert_eq!(activity_label(80, jobs_only, elapsed), jobs_timed);
assert_eq!(activity_label(80, parent, None), parent_plain);
}
#[test]
fn activity_status_labels_compress_background_counts() {
let spinner = LoadingSpinner::FRAMES[0];
let cases = [
(
ActivityStatus::Parent {
phase: ActivityPhase::RunningTool,
retry: None,
background: counts(2, 1),
},
vec![
format!("{spinner} running tool · 2 agents · 1 job"),
format!("{spinner} running tool · 2+1"),
format!("{spinner} 2+1"),
spinner.into(),
],
),
(
ActivityStatus::Parent {
phase: ActivityPhase::RunningTool,
retry: None,
background: counts(0, 3),
},
vec![
format!("{spinner} running tool · 3 jobs"),
format!("{spinner} running tool · 3"),
format!("{spinner} 3"),
spinner.into(),
],
),
(
ActivityStatus::Background(counts(1, 0)),
vec![
format!("{spinner} 1 agent working"),
format!("{spinner} 1 agent"),
format!("{spinner} 1"),
spinner.into(),
],
),
(
ActivityStatus::Background(counts(0, 1)),
vec![
format!("{spinner} 1 job running"),
format!("{spinner} 1 job"),
format!("{spinner} 1"),
spinner.into(),
],
),
(
ActivityStatus::Background(counts(2, 1)),
vec![
format!("{spinner} 2 agents · 1 job"),
format!("{spinner} 2+1"),
spinner.into(),
],
),
];
for (status, expected) in cases {
assert_eq!(activity_status_labels(status), expected);
}
}
#[test]
fn select_capped_rail_rows_prioritizes_live_then_failures() {
#[derive(Debug, PartialEq, Eq)]
struct Row {
id: &'static str,
live: bool,
fail: bool,
}
let rows = [
Row {
id: "ok-linger",
live: false,
fail: false,
},
Row {
id: "live-a",
live: true,
fail: false,
},
Row {
id: "fail-linger",
live: false,
fail: true,
},
Row {
id: "live-b",
live: true,
fail: false,
},
];
let (indices, hidden) = select_capped_rail_rows(&rows, 8, |row| row.live, |row| row.fail);
assert_eq!(indices, [1]);
assert_eq!(hidden, Some(3));
assert_eq!(rows[indices[0]].id, "live-a");
let lingering = [
Row {
id: "ok",
live: false,
fail: false,
},
Row {
id: "fail",
live: false,
fail: true,
},
Row {
id: "ok-2",
live: false,
fail: false,
},
];
let (indices, hidden) = select_capped_rail_rows(&lingering, 8, |row| row.live, |row| row.fail);
assert_eq!(indices, [1]);
assert_eq!(hidden, Some(2));
}
#[test]
fn overflow_label_singular_and_plural() {
assert_eq!(overflow_label(1, "agent", "agents"), "1 more agent");
assert_eq!(overflow_label(2, "job", "jobs"), "2 more jobs");
}