use super::*;
#[test]
fn subagent_card_selection_stays_stable_when_other_tasks_update() {
let mut state = MissionControlState::default();
let batch = append_subagent_card_batch(
&mut state,
"follow-batch",
&[ActivityStatus::Running, ActivityStatus::Running],
);
assert_eq!(
project_cards(&state)[0]
.subagent_card
.as_ref()
.and_then(|subagent_card| subagent_card.selected_task_id.as_deref()),
Some("g1")
);
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("follow-batch/g1"),
preview: "g1 newest output".to_string(),
});
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("follow-batch/g2"),
preview: "g2 later output".to_string(),
});
assert_eq!(
project_cards(&state)[0]
.subagent_card
.as_ref()
.and_then(|subagent_card| subagent_card.selected_task_id.as_deref()),
Some("g1")
);
state.pin_subagent_card_task(batch.clone(), ActivityId::new("follow-batch/g2"));
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("follow-batch/g1"),
preview: "\nstill newest".to_string(),
});
assert_eq!(
project_cards(&state)[0]
.subagent_card
.as_ref()
.and_then(|subagent_card| subagent_card.selected_task_id.as_deref()),
Some("g2")
);
}
#[test]
fn subagent_card_tail_uses_latest_own_and_descendant_lines_without_prompt_context() {
let mut state = MissionControlState {
subagent_card_rows: 5,
..Default::default()
};
let batch = append_subagent_card_batch(&mut state, "tail-batch", &[ActivityStatus::Running]);
let task = ActivityId::new("tail-batch/g1");
state.apply_activity_event(ActivityEvent::Delta {
id: task.clone(),
preview: "initial one\ninitial two\nown final one\nown final two".to_string(),
});
let tool = ActivityId::new("tail-batch/g1/tool-1");
state.apply_activity_event(ActivityEvent::Started {
id: tool.clone(),
parent_id: Some(task.clone()),
kind: ActivityKind::Tool,
status: ActivityStatus::Success,
metadata: ActivityMetadata::new("Read · safe.rs"),
});
state.apply_activity_event(ActivityEvent::Delta {
id: tool,
preview: "tool initial\ntool final".to_string(),
});
let card = &project_cards(&state)[0];
let tail = &card.children[0].live_tail;
assert!(
tail.iter().any(|line| line.contains("tool • final")),
"{tail:?}"
);
assert!(
tail.iter().any(|line| line.contains("own final two")),
"{tail:?}"
);
assert!(
!tail.iter().any(|line| line.contains("initial one")),
"{tail:?}"
);
let tool_final = tail
.iter()
.position(|line| line.contains("tool • final"))
.expect("tool final row");
let own_final = tail
.iter()
.position(|line| line.contains("own final two"))
.expect("own final row");
assert!(own_final < tool_final, "{tail:?}");
assert!(tail.len() <= 5);
state.pin_subagent_card_task(batch, task.clone());
assert!(state.open_subagent_card_transcript(task));
let detail = state.selected_activity_cached_text();
assert!(detail.contains("task intent 1"), "{detail}");
assert!(detail.contains("tool final"), "{detail}");
assert!(!detail.contains("FORBIDDEN CHILD PROMPT"), "{detail}");
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("later-root"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("Later root"),
});
assert_eq!(
state.selected_activity_id(),
Some(ActivityId::new("tail-batch/g1"))
);
}
#[test]
fn subagent_card_rows_setting_limits_projected_tail_and_activity_section() {
let mut state = MissionControlState {
subagent_card_rows: 2,
..Default::default()
};
append_subagent_card_batch(&mut state, "rows-batch", &[ActivityStatus::Running]);
let task = ActivityId::new("rows-batch/g1");
state.apply_activity_event(ActivityEvent::Delta {
id: task,
preview: "first\nsecond\nthird\nfourth\nfifth\nsixth".to_string(),
});
let card = &project_cards(&state)[0];
assert_eq!(card.children[0].live_tail.len(), 2);
let lines = visual_lines(&state, 80);
let activity_rows = lines
.iter()
.enumerate()
.filter(|(_, line)| line.copy_text.contains("agent •"))
.map(|(index, _)| index)
.collect::<Vec<_>>();
assert_eq!(activity_rows.len(), 2, "{lines:?}");
let controls_row = lines
.iter()
.position(|line| line.copy_text.contains("[View Subagent ↗]"))
.expect("view subagent controls row");
assert_eq!(controls_row, activity_rows[1] + 3, "{lines:?}");
}
#[test]
fn subagent_card_roster_is_two_rows_bounded_typed_and_responsive() {
let mut state = MissionControlState::default();
let statuses = [
ActivityStatus::Success,
ActivityStatus::Running,
ActivityStatus::Failed,
ActivityStatus::Queued,
ActivityStatus::Success,
ActivityStatus::Failed,
ActivityStatus::Queued,
ActivityStatus::Success,
];
append_subagent_card_batch(&mut state, "many-batch", &statuses);
for width in [1, 2, 12, 24, 48, 80] {
let lines = visual_lines(&state, width);
assert!(lines.iter().all(|line| {
UnicodeWidthStr::width(line_text(&line.line).as_str()) == usize::from(width)
}));
}
let lines = visual_lines(&state, 24);
let roster = lines
.iter()
.filter(|line| {
line.hit_regions.iter().any(|region| {
matches!(
region.target,
super::visual_lines::TranscriptHitTarget::SelectTask { .. }
)
})
})
.collect::<Vec<_>>();
assert!(roster.len() <= 2, "{lines:?}");
assert!(
lines.iter().any(|line| line.copy_text.contains("more")),
"{lines:?}"
);
assert!(
lines
.iter()
.any(|line| line.copy_text.contains("[g1 agent-1 ✓]")),
"{lines:?}"
);
assert!(
lines
.iter()
.any(|line| line.copy_text.contains('+') && line.copy_text.contains("more")),
"{lines:?}"
);
for line in roster {
for region in &line.hit_regions {
assert!(region.columns.start < region.columns.end);
assert!(region.columns.end <= 24);
}
}
}
#[test]
fn subagent_card_hit_targets_keep_multiple_batches_exact() {
let mut state = MissionControlState::default();
append_subagent_card_batch(&mut state, "batch-a", &[ActivityStatus::Running]);
append_subagent_card_batch(&mut state, "batch-b", &[ActivityStatus::Running]);
let targets = visual_lines(&state, 80)
.into_iter()
.flat_map(|line| line.hit_regions)
.filter_map(|region| match region.target {
super::visual_lines::TranscriptHitTarget::SelectTask { batch_id, task_id } => {
Some((batch_id, task_id))
}
_ => None,
})
.collect::<Vec<_>>();
assert!(targets.contains(&(ActivityId::new("batch-a"), ActivityId::new("batch-a/g1"))));
assert!(targets.contains(&(ActivityId::new("batch-b"), ActivityId::new("batch-b/g1"))));
}
#[test]
fn subagent_card_selection_keeps_roster_order_and_highlights_selected_tab() {
let mut state = MissionControlState::default();
let batch = append_subagent_card_batch(
&mut state,
"stable-roster",
&[
ActivityStatus::Running,
ActivityStatus::Running,
ActivityStatus::Running,
],
);
let roster_text = |state: &MissionControlState| {
visual_lines(state, 80)
.into_iter()
.filter(|line| {
line.hit_regions.iter().any(|region| {
matches!(
region.target,
super::visual_lines::TranscriptHitTarget::SelectTask { .. }
)
})
})
.map(|line| line.copy_text)
.collect::<Vec<_>>()
.join("\n")
};
let initial = roster_text(&state);
assert!(initial.find("g1").unwrap() < initial.find("g2").unwrap());
assert!(initial.find("g2").unwrap() < initial.find("g3").unwrap());
state.pin_subagent_card_task(batch.clone(), ActivityId::new("stable-roster/g1"));
let pinned = visual_lines(&state, 80);
assert_eq!(roster_text(&state), initial);
let g1 = pinned
.iter()
.flat_map(|line| &line.line.spans)
.find(|span| span.content == "[g1 agent-1 ●]")
.expect("g1 chip");
let g3 = pinned
.iter()
.flat_map(|line| &line.line.spans)
.find(|span| span.content == "[g3 agent-3 ●]")
.expect("g3 chip");
assert!(g1.style.add_modifier.contains(Modifier::REVERSED));
assert!(!g3.style.add_modifier.contains(Modifier::REVERSED));
assert!(g1.style.add_modifier.contains(Modifier::BOLD));
assert!(g3.style.add_modifier.contains(Modifier::BOLD));
let heading = pinned
.iter()
.flat_map(|line| &line.line.spans)
.find(|span| span.content.starts_with("● g1 ·"))
.expect("selected task heading");
assert_eq!(heading.style.fg, g1.style.fg);
assert!(
!pinned
.iter()
.any(|line| line.copy_text.contains("Follow live"))
);
}
#[test]
fn subagent_card_tail_is_five_newest_activities_in_ascending_order() {
let mut state = MissionControlState {
subagent_card_rows: 5,
..Default::default()
};
let batch = append_subagent_card_batch(&mut state, "ordered-tail", &[ActivityStatus::Running]);
let task = ActivityId::new("ordered-tail/g1");
for index in 1..=5 {
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new(format!("ordered-tail/g1/tool-{index}")),
parent_id: Some(task.clone()),
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(format!(
"activity {index} {}",
"writes without wrapping ".repeat(8)
)),
});
}
state.pin_subagent_card_task(batch.clone(), task.clone());
let card = &project_cards(&state)[0];
let tail = &card.children[0].live_tail;
assert_eq!(tail.len(), 5, "{tail:?}");
for (row, expected) in tail.iter().zip([1, 2, 3, 4, 5]) {
assert!(row.contains(&format!("activity {expected}")), "{tail:?}");
}
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("ordered-tail/g1/tool-6"),
parent_id: Some(task),
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(format!(
"activity 6 {}",
"writes without wrapping ".repeat(8)
)),
});
let tail = &project_cards(&state)[0].children[0].live_tail;
assert_eq!(tail.len(), 5, "{tail:?}");
for (row, expected) in tail.iter().zip([2, 3, 4, 5, 6]) {
assert!(row.contains(&format!("activity {expected}")), "{tail:?}");
}
for width in [12, 16, 24, 48] {
let lines = visual_lines(&state, width);
let activity_rows = lines
.iter()
.filter(|line| {
line.line.spans.iter().any(|span| {
span.style.bg == Some(state.theme.surface_background())
&& span.content.starts_with("too")
})
})
.collect::<Vec<_>>();
assert_eq!(activity_rows.len(), 5, "width {width}: {lines:?}");
assert!(
activity_rows
.iter()
.all(|line| visual_line_rows(line, width) == 1),
"width {width}: {activity_rows:?}"
);
}
let lines = visual_lines(&state, 80);
let chip = lines
.iter()
.find(|line| line.copy_text.contains("[g1 agent-1 ●]"))
.expect("chip row");
assert!(chip.copy_text.starts_with(" [g1 agent-1 ●]"));
assert_eq!(chip.hit_regions[0].columns.start, 3);
assert!(
lines
.iter()
.any(|line| line.copy_text.starts_with(" │ tool •"))
);
}
#[test]
fn subagent_card_tail_includes_assistant_reasoning_and_arbitrary_id_tool_activities() {
let mut state = MissionControlState::default();
append_subagent_card_batch(&mut state, "coverage-batch", &[ActivityStatus::Running]);
let task = ActivityId::new("coverage-batch/g1");
add_subagent_card_assistant_activity(
&mut state,
&task,
"assistant-arbitrary-id",
"assistant visible answer",
);
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("reasoning-arbitrary-id"),
parent_id: Some(task.clone()),
kind: ActivityKind::Assistant,
status: ActivityStatus::Success,
metadata: ActivityMetadata::reasoning(&["reasoning visible summary".to_string()]),
});
add_subagent_card_tool_activity(&mut state, &task, "call-bash", "bash", "cargo check");
add_subagent_card_tool_activity(&mut state, &task, "call-find", "find", "Cargo.toml");
add_subagent_card_tool_activity(&mut state, &task, "call-read", "read", "src/lib.rs");
let tail = &project_cards(&state)[0].children[0].live_tail;
assert_eq!(tail.len(), 5, "{tail:?}");
assert!(tail[0].contains("assistant visible answer"), "{tail:?}");
assert!(tail[1].contains("reasoning visible summary"), "{tail:?}");
assert!(tail[2].contains("bash • cargo check"), "{tail:?}");
assert!(tail[3].contains("find • Cargo.toml"), "{tail:?}");
assert!(tail[4].contains("read • src/lib.rs"), "{tail:?}");
let joined = tail.join("\n");
assert!(!joined.contains("FORBIDDEN"), "{joined}");
}
#[test]
fn subagent_card_stage_reserves_sixteen_activity_rows_and_spacer_before_controls() {
fn control_distance(state: &MissionControlState) -> usize {
let lines = visual_lines(state, 80);
let intent = lines
.iter()
.position(|line| line.copy_text.contains("task intent 1"))
.expect("intent row");
let controls = lines
.iter()
.position(|line| line.copy_text.contains("[View Subagent ↗]"))
.expect("controls row");
controls.saturating_sub(intent)
}
let mut zero = MissionControlState::default();
let zero_batch = ActivityId::new("fixed-zero");
zero.transcript
.push_back("tool: ⟳ subagents • running".to_string());
zero.transcript.link_activity(
0,
TranscriptActivityLink {
activity_id: zero_batch.clone(),
tool_name: "subagents".to_string(),
},
);
zero.apply_activity_event(ActivityEvent::Started {
id: zero_batch,
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata {
label: "subagents • 0 tasks".to_string(),
detail: None,
fields: vec![("tasks".to_string(), "0".to_string())],
},
});
let mut empty = MissionControlState::default();
append_subagent_card_batch(&mut empty, "fixed-empty", &[ActivityStatus::Running]);
assert_eq!(control_distance(&empty), 21);
assert!(
visual_lines(&zero, 80)
.iter()
.all(|line| line.hit_regions.is_empty())
);
let mut one = MissionControlState::default();
append_subagent_card_batch(&mut one, "fixed-one", &[ActivityStatus::Running]);
add_subagent_card_assistant_activity(
&mut one,
&ActivityId::new("fixed-one/g1"),
"single-assistant",
"one activity",
);
assert_eq!(control_distance(&one), 21);
let mut sixteen = MissionControlState::default();
append_subagent_card_batch(&mut sixteen, "fixed-sixteen", &[ActivityStatus::Running]);
let task = ActivityId::new("fixed-sixteen/g1");
for index in 1..=16 {
add_subagent_card_tool_activity(
&mut sixteen,
&task,
&format!("fixed-tool-{index}"),
"read",
&format!("file-{index}.rs"),
);
}
assert_eq!(control_distance(&sixteen), 21);
let lines = visual_lines(&sixteen, 80);
let controls = lines
.iter()
.position(|line| line.copy_text.contains("[View Subagent ↗]"))
.expect("controls row");
assert!(lines[controls - 1].copy_text.trim().is_empty());
}