use super::*;
#[test]
fn tool_activity_started_before_tool_started_links_row_to_existing_node() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
);
let id = ActivityId::new("call_read");
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("read src/lib.rs"),
});
apply_tool_started(&mut state, call.clone(), "read src/lib.rs");
apply_tool_result(
&mut state,
call,
test_tool_result(
"read",
true,
"file contents",
serde_json::json!({"bytes":13}),
),
);
assert_eq!(state.roots, vec![id.clone()]);
assert_eq!(state.transcript.len(), 1);
assert_eq!(
state
.transcript_activity_links
.get(&0)
.map(|link| &link.activity_id),
Some(&id)
);
assert_eq!(state.nodes[&id].status, ActivityStatus::Success);
assert!(state.transcript[0].contains("tool: ✓ read"));
}
#[test]
fn interleaved_thinking_and_assistant_complete_update_existing_cards() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "R1".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "A1".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "R1".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "A1".to_string(),
});
assert_eq!(state.transcript, vec!["thinking: R1", "assistant: A1"]);
}
#[test]
fn assistant_complete_updates_assistant_card_after_interleaved_thinking() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "A1".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "R".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "A2".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "A1A2".to_string(),
});
assert_eq!(
state
.transcript
.iter()
.filter(|entry| entry.starts_with("assistant: "))
.count(),
1
);
assert_eq!(state.transcript, vec!["assistant: A1A2", "thinking: R"]);
}
#[test]
fn assistant_complete_before_tool_boundary_updates_existing_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "old".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "old new".to_string(),
});
apply_tool_started(
&mut state,
test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
),
"read src/lib.rs",
);
assert_eq!(
state
.transcript
.iter()
.filter(|entry| entry.starts_with("assistant: "))
.count(),
1
);
assert_eq!(state.transcript.len(), 2);
assert_eq!(state.transcript[0], "assistant: old new");
assert!(state.transcript[1].contains("tool: ⟳ read src/lib.rs"));
}
#[test]
fn thinking_complete_after_hard_boundary_appends_new_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "old".to_string(),
});
state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".to_string(),
message: "boundary".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "old new".to_string(),
});
assert_eq!(
state.transcript,
vec!["thinking: old", "warning: boundary", "thinking: old new"]
);
}
#[test]
fn identified_completion_claims_active_delta_row_and_replaces_draft() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "draft".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: "final".to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
});
assert_eq!(state.transcript, vec!["thinking: final"]);
assert_eq!(
state
.thinking_transcript_identities
.get(&ThinkingTranscriptIdentity {
item_id: "rs_1".to_string(),
turn_id: Some("turn_1".to_string()),
}),
Some(&0)
);
}
#[test]
fn identified_completion_updates_same_identity_without_appending() {
let mut state = MissionControlState::default();
for text in ["draft", "final", "final revised"] {
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: text.to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
});
}
assert_eq!(state.transcript, vec!["thinking: final revised"]);
}
#[test]
fn identified_equal_text_items_remain_distinct_rows() {
let mut state = MissionControlState::default();
for item_id in ["rs_1", "rs_2"] {
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: "same".to_string(),
item_id: Some(item_id.to_string()),
turn_id: Some("turn_1".to_string()),
});
}
assert_eq!(state.transcript, vec!["thinking: same", "thinking: same"]);
assert_eq!(state.thinking_transcript_identities.len(), 2);
}
#[test]
fn identified_completion_with_stale_same_segment_index_recovers_active_row() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "draft".to_string(),
});
state.active_thinking_transcript_index = Some(1);
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: "final".to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
});
assert_eq!(state.transcript, vec!["thinking: final"]);
}
#[test]
fn identified_completion_with_stale_non_thinking_index_appends_once() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "draft".to_string(),
});
state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".to_string(),
message: "mutation".to_string(),
});
state.active_thinking_transcript_index = Some(1);
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: "final".to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
});
assert_eq!(
state.transcript,
vec!["thinking: draft", "warning: mutation", "thinking: final"]
);
}
#[test]
fn identified_identity_keys_do_not_collide_on_delimiters_or_missing_turn() {
let mut state = MissionControlState::default();
for (item_id, turn_id) in [
("a::b", Some("c")),
("b", Some("a::c")),
("current", None),
("current", Some("current")),
] {
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: item_id.to_string(),
item_id: Some(item_id.to_string()),
turn_id: turn_id.map(str::to_string),
});
}
assert_eq!(state.transcript.len(), 4);
assert_eq!(state.thinking_transcript_identities.len(), 4);
}
#[test]
fn identified_completion_does_not_overwrite_non_thinking_identity_target() {
let mut state = MissionControlState::default();
let event = || OutputEvent::ThinkingSummaryCompleteIdentified {
text: "final".to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
};
state.apply_output_event(&event());
state.transcript[0] = "warning: replaced externally".to_string();
state.apply_output_event(&event());
assert_eq!(
state.transcript,
vec!["warning: replaced externally", "thinking: final"]
);
}
#[test]
fn identified_completion_after_hard_boundary_does_not_claim_old_row() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "old".to_string(),
});
state.apply_output_event(&OutputEvent::Diagnostic {
level: "warning".to_string(),
message: "boundary".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryCompleteIdentified {
text: "new".to_string(),
item_id: Some("rs_1".to_string()),
turn_id: Some("turn_1".to_string()),
});
assert_eq!(
state.transcript,
vec!["thinking: old", "warning: boundary", "thinking: new"]
);
}
#[test]
fn sequential_thinking_then_assistant_behavior_is_preserved() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "reason".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "reason final".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "answer".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer final".to_string(),
});
assert_eq!(
state.transcript,
vec!["thinking: reason final", "assistant: answer final"]
);
}
#[test]
fn multiple_thinking_segments_before_assistant_do_not_duplicate() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "R1".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "R1 final".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "R2".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "A".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "R2 final".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "A final".to_string(),
});
assert_eq!(
state.transcript,
vec![
"thinking: R1 final",
"thinking: R2 final",
"assistant: A final"
]
);
}
#[test]
fn tool_result_upserts_final_preview_without_duplicate_activity_root() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
);
let id = ActivityId::new("call_read");
apply_tool_started(&mut state, call.clone(), "read src/lib.rs");
apply_tool_result(
&mut state,
call,
test_tool_result(
"read",
true,
"file contents",
serde_json::json!({"bytes":13}),
),
);
state.apply_activity_event(ActivityEvent::FinalPreview {
id: id.clone(),
preview: "final preview".to_string(),
metadata: Some(ActivityMetadata::new("read src/lib.rs")),
status: Some(ActivityStatus::Success),
});
assert_eq!(state.roots, vec![id.clone()]);
assert_eq!(state.visible_nodes(), vec![(0, id.clone())]);
assert_eq!(state.nodes[&id].preview, "final preview");
assert_eq!(state.nodes[&id].status, ActivityStatus::Success);
}
#[test]
fn transcript_skips_whitespace_only_assistant_output() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "\n\n".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "\n\n".to_string(),
});
assert!(state.transcript.is_empty());
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "\nvisible\n".to_string(),
});
assert_eq!(state.transcript, vec!["assistant: \nvisible\n"]);
}
#[test]
fn transcript_cache_invalidates_after_assistant_delta_and_label_change() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "**hello**".to_string(),
});
let visual = state.transcript_visual_text();
assert!(visual.contains("🤖Assistant …"));
assert!(state.transcript_cache.borrow().visual_text.is_some());
state.finish_assistant_streaming();
assert!(state.transcript_cache.borrow().visual_text.is_none());
assert!(state.transcript_visual_text().contains("🤖Assistant\n"));
assert!(!state.transcript_visual_text().contains("🤖Assistant …"));
}
#[test]
fn transcript_cache_invalidates_after_tool_error_and_reset_entries() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "hi".to_string(),
});
let _ = state.cached_transcript_lines(80);
let _ = state.transcript_visual_rows(8);
assert!(
state
.transcript_cache
.borrow()
.visual_lines
.contains_key(&80)
);
assert!(state.transcript_cache.borrow().visual_rows.contains_key(&8));
apply_tool_started(
&mut state,
test_tool_call(
"call_write",
"write",
serde_json::json!({"path":"file.txt","content":"hi"}),
),
"write file.txt",
);
assert!(state.transcript_cache.borrow().visual_lines.is_empty());
let _ = state.transcript_visual_text();
state.record_error_transcript("boom");
assert!(state.transcript_cache.borrow().visual_text.is_none());
let _ = state.transcript_visual_rows(8);
state.reset_for_new_session();
assert!(state.transcript_cache.borrow().visual_rows.is_empty());
}
#[test]
fn cached_transcript_lines_match_uncached_projection() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "show markdown".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "# Title\n\n- item".to_string(),
});
let cached = state.cached_transcript_lines(80);
let uncached = crate::tui::render::transcript::uncached_transcript_lines(&state, 80);
assert_eq!(
cached_render_line_text(&cached),
cached_render_line_text(&uncached)
);
}
#[test]
fn cached_transcript_lines_reuses_width_cache() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "one".to_string(),
});
let first = state.cached_transcript_lines(80);
assert!(
state
.transcript_cache
.borrow()
.visual_lines
.contains_key(&80)
);
let second = state.cached_transcript_lines(80);
assert_eq!(
cached_render_line_text(&first),
cached_render_line_text(&second)
);
let narrow = state.cached_transcript_lines(20);
assert!(
state
.transcript_cache
.borrow()
.visual_lines
.contains_key(&20)
);
assert_ne!(
cached_render_line_text(&second),
cached_render_line_text(&narrow)
);
assert!(cached_render_line_text(&second).contains('┌'));
assert!(!cached_render_line_text(&narrow).contains('┌'));
}
#[test]
fn cached_visual_line_lookup_uses_generation_cache_key() {
let mut state = MissionControlState::default();
for index in 0..50 {
state.apply_output_event(&OutputEvent::AssistantComplete {
text: format!("answer {index}"),
});
}
let _ = state.cached_transcript_lines(80);
let generation = state.transcript_visual_generation();
reset_project_entry_count_for_test();
let _ = state.cached_transcript_lines(80);
assert_eq!(state.transcript_visual_generation(), generation);
assert_eq!(project_entry_count_for_test(), 0);
assert_eq!(
state
.transcript_cache
.borrow()
.visual_lines
.get(&80)
.map(|cached| cached.generation),
Some(generation)
);
}
#[test]
fn appending_transcript_entry_changes_generation_and_refreshes_cache() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first".to_string(),
});
let first = cached_render_line_text(&state.cached_transcript_lines(80));
let generation = state.transcript_visual_generation();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "answer".to_string(),
});
let second = cached_render_line_text(&state.cached_transcript_lines(80));
assert_ne!(state.transcript_visual_generation(), generation);
assert!(first.contains("first"));
assert!(second.contains("answer"));
}
#[test]
fn active_assistant_row_mutation_changes_generation_and_refreshes_cache() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "draft".to_string(),
});
let _ = state.cached_transcript_lines(80);
let generation = state.transcript_visual_generation();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: " update".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_ne!(state.transcript_visual_generation(), generation);
assert!(rendered.contains("draft update"), "{rendered}");
}
#[test]
fn linked_activity_update_changes_generation_only_for_affected_entries() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
);
let id = ActivityId::new(call.id.clone());
apply_tool_started(&mut state, call, "read src/lib.rs");
let _ = state.cached_transcript_lines(80);
let generation = state.transcript_visual_generation();
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("unrelated"),
preview: "noise".to_string(),
});
assert_eq!(state.transcript_visual_generation(), generation);
state.apply_activity_event(ActivityEvent::Delta {
id,
preview: "fresh preview".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_ne!(state.transcript_visual_generation(), generation);
assert!(rendered.contains("fresh preview"), "{rendered}");
}
#[test]
fn activity_events_reproject_only_affected_linked_transcript_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "historical answer".to_string(),
});
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
);
let id = ActivityId::new(call.id.clone());
apply_tool_started(&mut state, call, "read src/lib.rs");
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("read src/lib.rs"),
});
state.apply_activity_event(ActivityEvent::Delta {
id: id.clone(),
preview: "initial preview".to_string(),
});
let _ = state.cached_transcript_lines(80);
reset_project_entry_count_for_test();
state.apply_activity_event(ActivityEvent::Delta {
id: id.clone(),
preview: "\nfresh preview".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_eq!(project_entry_count_for_test(), 1);
assert!(rendered.contains("historical answer"), "{rendered}");
assert!(rendered.contains("fresh preview"), "{rendered}");
reset_project_entry_count_for_test();
state.apply_activity_event(ActivityEvent::Finished {
id: id.clone(),
status: ActivityStatus::Success,
metadata: None,
});
let _ = state.cached_transcript_lines(80);
assert_eq!(project_entry_count_for_test(), 1);
assert_eq!(
project_cards(&state).get(2).map(|card| card.status),
Some(crate::tui::transcript_cards::TranscriptCardStatus::Success)
);
reset_project_entry_count_for_test();
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("unrelated"),
preview: "noise".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_eq!(project_entry_count_for_test(), 0);
assert!(rendered.contains("historical answer"), "{rendered}");
assert!(rendered.contains("fresh preview"), "{rendered}");
}
#[test]
fn descendant_activity_event_reprojects_only_parent_linked_transcript_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "historical answer".to_string(),
});
let call = subagents_call();
let parent = ActivityId::new(call.id.clone());
let child = ActivityId::new("call_subagents/g1");
apply_tool_started(&mut state, call, "subagents • 2 tasks");
state.apply_activity_event(ActivityEvent::Started {
id: parent.clone(),
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("subagents • 2 tasks"),
});
state.apply_activity_event(ActivityEvent::Started {
id: child.clone(),
parent_id: Some(parent),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
let _ = state.cached_transcript_lines(80);
reset_project_entry_count_for_test();
state.apply_activity_event(ActivityEvent::Delta {
id: child,
preview: "child preview".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_eq!(project_entry_count_for_test(), 1);
assert!(rendered.contains("historical answer"), "{rendered}");
assert!(rendered.contains("inspect"), "{rendered}");
}
#[test]
fn cached_transcript_lines_reprojects_only_changed_transcript_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "historical answer".to_string(),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "second".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "draft".to_string(),
});
let _ = state.cached_transcript_lines(80);
reset_project_entry_count_for_test();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: " update".to_string(),
});
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_eq!(project_entry_count_for_test(), 1);
assert!(rendered.contains("historical answer"));
assert!(rendered.contains("draft update"));
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
);
apply_tool_started(&mut state, call.clone(), "read src/lib.rs");
let _ = state.cached_transcript_lines(80);
reset_project_entry_count_for_test();
apply_tool_result(
&mut state,
call,
test_tool_result("read", true, "done", serde_json::json!({"bytes":4})),
);
let rendered = cached_render_line_text(&state.cached_transcript_lines(80));
assert_eq!(project_entry_count_for_test(), 1);
assert!(rendered.contains("historical answer"));
assert!(rendered.contains("read"));
}
#[test]
fn transcript_selection_uses_cached_visual_text() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "hello world".to_string(),
});
let text = state.transcript_visual_text();
let start = text.find("hello").unwrap();
let end = start + "hello".len();
state.start_selection(TuiPane::Transcript, TextPosition::new(start, 0, 0));
state.update_selection(TuiPane::Transcript, TextPosition::new(end, 0, 5));
assert_eq!(
state
.finish_selection(
TuiPane::Transcript,
crate::tui::transcript_cards::CARD_LAYOUT_MIN_WIDTH
)
.as_deref(),
Some("hello")
);
}
#[test]
fn activity_tree_render_scroll_keeps_viewport_when_selection_is_offscreen() {
let mut state = MissionControlState::default();
for index in 0..10 {
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new(format!("tool-{index}")),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(format!("tool {index}")),
});
}
state.selected = 0;
state.set_scroll_offset(&state.scroll_views.activity_tree, 5);
assert_eq!(state.activity_tree_render_scroll(3), 5);
assert_eq!(state.selected_activity_id().unwrap().as_str(), "tool-0");
}
#[test]
fn activity_tree_append_preserves_manual_selection() {
let mut state = MissionControlState::default();
for index in 0..4 {
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new(format!("tool-{index}")),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(format!("tool {index}")),
});
}
state.selected = 1;
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("tool-4"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("tool 4"),
});
assert_eq!(state.selected, 1);
}
#[test]
fn write_tool_transcript_pending_row_resolves_to_success_without_duplicate() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_write",
"write",
serde_json::json!({"path":"file.txt","content":"secret payload"}),
);
apply_tool_started(&mut state, call.clone(), "write file.txt");
assert_eq!(state.transcript.len(), 1);
assert!(state.transcript[0].contains("tool: ⟳ write file.txt • writing • 14 bytes"));
assert!(!state.transcript[0].contains("secret payload"));
apply_tool_result(
&mut state,
call,
test_tool_result(
"write",
true,
"wrote 14 bytes",
serde_json::json!({"bytes":14}),
),
);
assert_eq!(state.transcript.len(), 1);
assert!(state.transcript[0].contains("tool: ✓ write file.txt • 14 bytes"));
assert!(state.active_tool_transcripts.is_empty());
}
#[test]
fn hash_edit_tool_transcript_pending_row_resolves_to_failure_without_duplicate() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_hash_edit",
"hash_edit",
serde_json::json!({"input":"[file.txt#ABCD]\nSWAP 1.=1:\n+other"}),
);
apply_tool_started(&mut state, call.clone(), "hash_edit");
assert_eq!(state.transcript.len(), 1);
assert!(state.transcript[0].contains("tool: ⟳ hash_edit • running • 33 bytes"));
assert!(!state.transcript[0].contains("SWAP"));
assert!(!state.transcript[0].contains("secret"));
apply_tool_result(
&mut state,
call,
test_tool_result("hash_edit", false, "missing text", serde_json::json!({})),
);
assert_eq!(state.transcript.len(), 1);
assert!(state.transcript[0].contains("tool: ✗ hash_edit"));
assert!(state.active_tool_transcripts.is_empty());
}
#[test]
fn subagents_transcript_start_creates_live_task_rows() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
assert_eq!(state.transcript.len(), 1);
let entry = &state.transcript[0];
assert!(entry.contains("tool: ⟳ subagents • 2 tasks • starting"));
assert!(entry.contains("├─ g1 • identity frontend-dev • queued"));
assert!(entry.contains("└─ g2 • agent reviewer • queued"));
}
#[test]
fn subagents_transcript_usage_update_renders_live_task_context() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
let task = ActivityId::new("call_subagents/g1");
state.apply_activity_event(ActivityEvent::Started {
id: task.clone(),
parent_id: Some(ActivityId::new("call_subagents")),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
state.apply_activity_event(ActivityEvent::UsageUpdate {
id: task.clone(),
current_tokens: 13_000,
max_tokens: 128_000,
reasoning_tokens: Some(99),
source: ContextUsageSource::ProviderExact,
request_sequence: 1,
});
state.apply_activity_event(ActivityEvent::UsageUpdate {
id: task,
current_tokens: 10,
max_tokens: 128_000,
reasoning_tokens: None,
source: ContextUsageSource::FallbackProjection,
request_sequence: 1,
});
let entry = &state.transcript[0];
assert!(entry.contains("g1 • identity frontend-dev • running • ctx 10%/128k • thinking 99"));
assert!(entry.contains("g2 • agent reviewer • queued"));
}
#[test]
fn subagents_result_transcript_includes_total_tokens() {
let mut state = MissionControlState::default();
let call = subagents_call();
apply_tool_started(&mut state, call.clone(), "subagents • 2 tasks");
apply_tool_result(
&mut state,
call,
test_tool_result(
"subagents",
true,
"child output sk-childSecret123456",
serde_json::json!({"summary":{"total":2,"completed":2,"failed":0,"total_tokens":12345}}),
),
);
let entry = &state.transcript[0];
assert!(entry.contains("12345 tokens"), "{entry}");
assert!(!entry.contains("sk-childSecret123456"), "{entry}");
}
#[test]
fn subagents_transcript_read_start_stays_single_line() {
let mut state = MissionControlState::default();
apply_tool_started(
&mut state,
test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"src/lib.rs"}),
),
"read src/lib.rs",
);
assert_eq!(state.transcript, vec!["tool: ⟳ read src/lib.rs • starting"]);
}
#[test]
fn subagents_transcript_tracks_recent_activity_and_bounds_to_three() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
let parent = ActivityId::new("call_subagents");
let task = ActivityId::new("call_subagents/g1");
state.apply_activity_event(ActivityEvent::Started {
id: task.clone(),
parent_id: Some(parent),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
for (index, label) in ["read a", "bash b", "assistant", "read d"]
.iter()
.enumerate()
{
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new(format!("call_subagents/g1/activity-{index}")),
parent_id: Some(task.clone()),
kind: if *label == "assistant" {
ActivityKind::Assistant
} else {
ActivityKind::Tool
},
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(*label),
});
}
let entry = &state.transcript[0];
assert!(entry.contains("g1 • identity frontend-dev • running"));
assert!(!entry.contains("started g1 inspect"));
assert!(!entry.contains("tool: read a"));
assert!(entry.contains("tool: bash b"));
assert!(entry.contains("assistant: assistant"));
assert!(entry.contains("tool: read d"));
assert_eq!(entry.matches(" │ • ").count(), 3);
}
#[test]
fn subagents_transcript_maps_parent_id_child_activity_to_owner_task() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
let parent = ActivityId::new("call_subagents");
let task = ActivityId::new("call_subagents/g1");
state.apply_activity_event(ActivityEvent::Started {
id: task.clone(),
parent_id: Some(parent),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("write_1"),
parent_id: Some(task),
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("write src/lib.rs"),
});
let entry = &state.transcript[0];
assert!(entry.contains("g1 • identity frontend-dev • running"));
assert!(entry.contains("tool: write src/lib.rs"));
assert!(!entry.contains("g2 • agent reviewer • running"));
}
#[test]
fn subagents_transcript_failed_task_updates_row_and_keeps_recent_bounded() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
let task = ActivityId::new("call_subagents/g1");
state.apply_activity_event(ActivityEvent::Started {
id: task.clone(),
parent_id: Some(ActivityId::new("call_subagents")),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
for index in 0..3 {
state.apply_activity_event(ActivityEvent::FinalPreview {
id: ActivityId::new(format!("call_subagents/g1/final-{index}")),
preview: "secret child output should not be used here".to_string(),
metadata: Some(ActivityMetadata {
label: format!("child {index}"),
detail: Some(format!("detail {index}")),
fields: Vec::new(),
}),
status: Some(ActivityStatus::Success),
});
}
state.apply_activity_event(ActivityEvent::Finished {
id: task,
status: ActivityStatus::Failed,
metadata: None,
});
let entry = &state.transcript[0];
assert!(entry.contains("g1 • identity frontend-dev • failed"));
assert!(entry.contains("finished: failed"));
assert!(!entry.contains("secret child output"));
assert!(entry.matches(" │ • ").count() <= 3);
}
#[test]
fn subagents_transcript_tool_result_replaces_live_rows() {
let mut state = MissionControlState::default();
apply_tool_started(&mut state, subagents_call(), "subagents • 2 tasks");
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_subagents/g1"),
parent_id: Some(ActivityId::new("call_subagents")),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("g1 inspect"),
});
apply_tool_result(
&mut state,
subagents_call(),
test_tool_result(
"subagents",
true,
"safe aggregate",
serde_json::json!({"completed":2}),
),
);
assert_eq!(state.transcript.len(), 1);
let entry = &state.transcript[0];
assert!(entry.starts_with("tool: ✓ subagents"));
assert!(!entry.contains("queued"));
assert!(!entry.contains("running"));
assert!(!entry.contains("g1 inspect"));
}
#[test]
fn multi_file_read_result_detail_and_transcript_show_path_values() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_read_multi",
"read",
serde_json::json!({"paths":["a.txt", "b.txt"],"limit":2}),
);
apply_tool_result(
&mut state,
call,
test_tool_result(
"read",
true,
"--- FILE: a.txt ---\na\n--- FILE: b.txt ---\nb",
serde_json::json!({}),
),
);
let detail = state.selected_activity_text();
assert!(detail.contains("Status: SUCCESS"), "{detail}");
assert!(detail.contains("\"paths\": ["), "{detail}");
assert!(detail.contains("\"a.txt\""), "{detail}");
assert!(detail.contains("\"b.txt\""), "{detail}");
assert!(!detail.contains("activity preview truncated"), "{detail}");
let visual = state.transcript_visual_text();
assert!(visual.contains("✓ read 2 files"), "{visual}");
assert!(!visual.contains("TOOL START"), "{visual}");
assert!(!visual.contains("activity preview truncated"), "{visual}");
}
#[test]
fn thinking_summary_event_appends_distinct_transcript_entry() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "provider summary".to_string(),
});
assert_eq!(
state.transcript,
vec!["thinking: provider summary".to_string()]
);
assert!(
state
.transcript_visual_text()
.contains("🧠Reasoning Summary")
);
}
#[test]
fn thinking_summary_deltas_update_one_active_transcript_entry() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "think".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "ing".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "thinking".to_string(),
});
assert_eq!(state.transcript, vec!["thinking: thinking".to_string()]);
}
#[test]
fn user_prompt_starts_new_thinking_card_after_incomplete_stream() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "old".to_string(),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "next".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: "new".to_string(),
});
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "new".to_string(),
});
assert_eq!(
state.transcript,
vec![
"thinking: old".to_string(),
"you: next".to_string(),
"thinking: new".to_string(),
]
);
}
#[test]
fn context_usage_event_updates_state_without_transcript_line() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ContextUsage {
current_tokens: 12345,
max_tokens: 128000,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
});
assert_eq!(
state.context_usage,
Some(ContextUsageState {
current_tokens: 12345,
max_tokens: 128000,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
})
);
assert!(state.transcript.is_empty());
}
#[test]
fn error_transcript_rows_are_sanitized_compact_and_deduplicated() {
let mut state = MissionControlState::default();
state.record_error_transcript(
"\u{1b}[31mprovider failed\u{1b}[0m\ntoken=tokensecret\napi_key='secret-value'\nignored detail",
);
state.record_error_transcript(
"provider failed\ntoken=tokensecret\napi_key='secret-value'\nignored detail",
);
assert_eq!(state.transcript.len(), 1);
assert_eq!(
state.transcript[0],
"error: provider failed • token=<redacted> • api_key=<redacted>"
);
assert!(!state.transcript[0].contains('\u{1b}'));
assert!(!state.transcript[0].contains("tokensecret"));
assert!(!state.transcript[0].contains("secret-value"));
assert!(!state.transcript[0].contains("ignored detail"));
}
#[test]
fn prompt_bash_and_session_header_transcript_rows_are_sanitized_and_redacted() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::SessionHeader {
session_id: Some("session".to_string()),
model: "gpt\u{1b}[31m api_key='model-secret'".to_string(),
cwd: std::path::PathBuf::from("/tmp/Bearer cwdBearerSecret"),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "inspect\u{1b}[0m api_key='prompt-secret' Bearer promptBearerSecret".to_string(),
});
state.apply_output_event(&OutputEvent::BashCommand {
command: "curl -H 'Authorization: Bearer bashBearerSecret' api_key=bash-secret".to_string(),
});
let transcript = state.transcript.join("\n");
assert!(transcript.contains("session: gpt"), "{transcript}");
assert!(transcript.contains("you: inspect"), "{transcript}");
assert!(transcript.contains("bash: curl"), "{transcript}");
assert!(!transcript.contains('\u{1b}'), "{transcript}");
assert!(!transcript.contains("model-secret"), "{transcript}");
assert!(!transcript.contains("cwdBearerSecret"), "{transcript}");
assert!(!transcript.contains("prompt-secret"), "{transcript}");
assert!(!transcript.contains("promptBearerSecret"), "{transcript}");
assert!(!transcript.contains("bashBearerSecret"), "{transcript}");
assert!(!transcript.contains("bash-secret"), "{transcript}");
assert!(
transcript.contains("<redacted>") || transcript.contains("[REDACTED]"),
"{transcript}"
);
}
#[test]
fn transient_running_status_does_not_add_error_transcript_spam() {
let mut state = MissionControlState::default();
state.record_error_transcript("running prompt…");
assert!(state.transcript.is_empty());
}
#[test]
fn consecutive_assistant_deltas_append_to_one_transcript_message() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Hel".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "lo".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: " world".to_string(),
});
assert_eq!(state.transcript, vec!["assistant: Hello world".to_string()]);
}
#[test]
fn assistant_deltas_redact_split_secret_preview_without_buffering_safe_text() {
let secret = "sentinelAssistantPreview123";
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Answer: ".to_string(),
});
assert_eq!(state.transcript, vec!["assistant: Answer: ".to_string()]);
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "api_".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "key=".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: format!("{secret}\n"),
});
let joined = state.transcript.join("\n");
assert!(joined.contains("api_key=<redacted>"), "{joined}");
assert!(!joined.contains(secret), "{joined}");
}
#[test]
fn transcript_visual_text_adds_scannable_role_blocks() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "inspect\nthen summarize".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "I'll read.".to_string(),
});
assert_eq!(
state.transcript_visual_text(),
"👤You\n inspect\n then summarize\n\n🤖Assistant …\n I'll read."
);
}
#[test]
fn assistant_complete_restores_normal_transcript_label() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Streaming".to_string(),
});
assert!(state.assistant_streaming);
assert_eq!(state.transcript_visual_text(), "🤖Assistant …\n Streaming");
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "Streaming done".to_string(),
});
assert!(!state.assistant_streaming);
assert_eq!(
state.transcript_visual_text(),
"🤖Assistant\n Streaming done"
);
}
#[test]
fn transcript_run_finish_reset_clears_streaming_assistant_label() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Still running".to_string(),
});
state.finish_assistant_streaming();
assert!(!state.assistant_streaming);
assert_eq!(
state.transcript_visual_text(),
"🤖Assistant\n Still running"
);
}
#[test]
fn transcript_visual_text_adds_separator_before_later_user_turns() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "first".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "answer".to_string(),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "second".to_string(),
});
assert_eq!(
state.transcript_visual_text(),
"👤You\n first\n\n🤖Assistant\n answer\n\n── user turn ──\n👤You\n second"
);
}
#[test]
fn user_prompt_starts_new_transcript_boundary_after_assistant_delta() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "First".to_string(),
});
state.apply_output_event(&OutputEvent::UserPrompt {
text: "Next prompt".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Second".to_string(),
});
assert_eq!(
state.transcript,
vec![
"assistant: First".to_string(),
"you: Next prompt".to_string(),
"assistant: Second".to_string(),
]
);
}
#[test]
fn transcript_render_scroll_tails_overflow_and_supports_manual_scrollback() {
let mut state = MissionControlState::default();
for line in 0..10 {
state.push_transcript(format!("line {line}"));
}
let expected_overflow = state.transcript_visual_rows(80).saturating_sub(4);
assert_eq!(state.transcript_render_scroll(4, 80), expected_overflow);
assert!(state.scroll_transcript_mouse_up(4, 80));
assert_eq!(
state.transcript_scroll_offset(),
usize::from(MOUSE_WHEEL_SCROLL_ROWS)
);
assert_eq!(
state.transcript_render_scroll(4, 80),
expected_overflow.saturating_sub(usize::from(MOUSE_WHEEL_SCROLL_ROWS))
);
assert!(state.scroll_transcript_mouse_down(4, 80));
assert_eq!(state.transcript_scroll_offset(), 0);
assert_eq!(state.transcript_render_scroll(4, 80), expected_overflow);
}
#[test]
fn transcript_render_scroll_counts_wrapped_visual_rows() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "abcdefghijabcdefghijabcdefghijabcdefghij".to_string(),
});
let visual_rows = state.transcript_visual_rows(10);
let initial_scroll = state.transcript_render_scroll(3, 10);
assert!(visual_rows > 5);
assert!(initial_scroll > 0);
assert!(state.scroll_transcript_mouse_up(3, 10));
assert!(state.transcript_render_scroll(3, 10) < initial_scroll);
}
#[test]
fn transcript_render_scroll_counts_word_wrapped_rows() {
let state = MissionControlState {
transcript: vec![
"assistant: alpha beta gamma delta epsilon zeta eta theta iota SENTINEL".to_string(),
]
.into(),
..Default::default()
};
let raw_rows = prompt::visual_rows(&state.transcript.join("\n"), 18);
let display_rows = state.transcript_visual_rows(18);
let expected_overflow = display_rows.saturating_sub(3);
assert_eq!(raw_rows, 4);
assert!(display_rows > raw_rows);
assert_eq!(state.transcript_scroll_overflow(3, 18), expected_overflow);
assert_eq!(state.transcript_render_scroll(3, 18), expected_overflow);
}
#[test]
fn transcript_render_scroll_keeps_usize_offsets_above_u16_max() {
let state = MissionControlState {
transcript: vec![format!("error: {}", "x".repeat(70_000))].into(),
..Default::default()
};
let scroll = state.transcript_render_scroll(1, 1);
assert!(scroll > usize::from(u16::MAX), "scroll was {scroll}");
}
#[test]
fn transcript_mouse_hit_testing_uses_word_wrap_for_spaces_and_long_words() {
let state = MissionControlState {
transcript: vec!["assistant: alpha beta supercalifragilistic".to_string()].into(),
..Default::default()
};
let visual = state.transcript_visual_text();
assert_eq!(state.transcript_visual_rows(8), 7);
assert_eq!(
state
.transcript_text_position_for_cell(2, 0, 10, 8)
.map(|position| position.byte),
Some(visual.find("alpha").unwrap().saturating_sub(2))
);
assert_eq!(
state
.transcript_text_position_for_cell(2, 2, 10, 8)
.map(|position| position.byte),
visual.find("alpha")
);
assert_eq!(
state
.transcript_text_position_for_cell(3, 0, 10, 8)
.map(|position| position.byte),
visual.find("beta")
);
assert_eq!(
state
.transcript_text_position_for_cell(4, 0, 10, 8)
.map(|position| position.byte),
visual.find("superca")
);
assert_eq!(
state
.transcript_text_position_for_cell(5, 0, 10, 8)
.map(|position| position.byte),
Some(visual.find("superca").unwrap() + 8)
);
}
#[test]
fn bash_tool_result_transcript_uses_compact_label_but_detail_keeps_command() {
let mut state = MissionControlState::default();
let command = "printf 'a very long command that belongs in detail, not transcript'";
let call = crate::providers::ToolCall {
id: "call_bash_long".to_string(),
name: "bash".to_string(),
arguments: serde_json::json!({"command": command}),
};
let result = crate::tools::ToolResult {
tool_name: "bash".to_string(),
success: true,
content: "ok\n".to_string(),
metadata: serde_json::json!({"exit_code":0}),
display: crate::tools::ToolResultDisplay::default(),
};
let summary = crate::output::tool_display_summary(&call, &result);
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(call),
result: Box::new(result),
summary: Box::new(summary),
});
assert_eq!(
state.transcript,
vec!["tool: ✓ bash • exit 0 • 1 line".to_string()]
);
assert!(!state.transcript[0].contains(command));
let node = state
.nodes
.get(&ActivityId::new("call_bash_long"))
.expect("tool result should upsert an activity node");
assert_eq!(node.metadata.label, format!("bash {command}"));
let detail = state.selected_activity_text();
assert!(detail.contains(&format!("Tool • bash • bash {command}")));
assert!(detail.contains("ok"));
}
#[test]
fn successful_read_tool_result_transcript_shows_status_label_and_lines() {
let mut state = MissionControlState::default();
let call = crate::providers::ToolCall {
id: "call_read_lines".to_string(),
name: "read".to_string(),
arguments: serde_json::json!({"path":"src/lib.rs"}),
};
let result = crate::tools::ToolResult {
tool_name: "read".to_string(),
success: true,
content: "one\ntwo\n".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let summary = crate::output::tool_display_summary(&call, &result);
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(call),
result: Box::new(result),
summary: Box::new(summary),
});
assert_eq!(
state.transcript,
vec!["tool: ✓ read src/lib.rs • 2 lines".to_string()]
);
}
#[test]
fn transcript_tool_metadata_uses_singular_and_plural_count_labels() {
assert_eq!(
transcript_tool_metadata_part("output_lines", "1"),
Some("1 line".to_string())
);
assert_eq!(
transcript_tool_metadata_part("output_lines", "2"),
Some("2 lines".to_string())
);
assert_eq!(
transcript_tool_metadata_part("edit_count", "1"),
Some("1 edit".to_string())
);
assert_eq!(
transcript_tool_metadata_part("edit_count", "3"),
Some("3 edits".to_string())
);
assert_eq!(
transcript_tool_metadata_part("matches_returned", "1"),
Some("1 match".to_string())
);
assert_eq!(
transcript_tool_metadata_part("matches_returned", "4"),
Some("4 matches".to_string())
);
assert_eq!(
transcript_tool_metadata_part("tasks", "1"),
Some("1 task".to_string())
);
assert_eq!(
transcript_tool_metadata_part("tasks", "5"),
Some("5 tasks".to_string())
);
assert_eq!(
transcript_tool_metadata_part("reference", "references/checklist.md"),
Some("reference references/checklist.md".to_string())
);
}
#[test]
fn subagents_transcript_omits_duplicate_task_metadata() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "call_subagents".to_string(),
name: "subagents".to_string(),
arguments: serde_json::json!({}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "subagents".to_string(),
success: true,
content: "done".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(crate::output::ToolDisplaySummary {
tool_name: "subagents".to_string(),
status: crate::output::ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "subagents • 2 tasks".to_string(),
metadata: vec![
("tasks".to_string(), "2".to_string()),
("completed".to_string(), "2".to_string()),
],
}),
});
assert_eq!(
state.transcript,
vec!["tool: ✓ subagents • 2 tasks • 2 completed".to_string()]
);
}
#[test]
fn subagents_transcript_includes_selected_task_identities() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "call_subagents_identity".to_string(),
name: "subagents".to_string(),
arguments: serde_json::json!({}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "subagents".to_string(),
success: true,
content: "done".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(crate::output::ToolDisplaySummary {
tool_name: "subagents".to_string(),
status: crate::output::ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "subagents • 2 tasks".to_string(),
metadata: vec![
("tasks".to_string(), "2".to_string()),
("identities".to_string(), "g1=frontend-dev".to_string()),
("completed".to_string(), "2".to_string()),
],
}),
});
assert_eq!(
state.transcript,
vec!["tool: ✓ subagents • 2 tasks • identities g1=frontend-dev • 2 completed".to_string()]
);
}
#[test]
fn subagents_transcript_uses_singular_task_label_without_duplicate_metadata() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "call_subagents_one".to_string(),
name: "subagents".to_string(),
arguments: serde_json::json!({}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "subagents".to_string(),
success: true,
content: "done".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(crate::output::ToolDisplaySummary {
tool_name: "subagents".to_string(),
status: crate::output::ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "subagents • 1 task".to_string(),
metadata: vec![
("tasks".to_string(), "1".to_string()),
("completed".to_string(), "1".to_string()),
],
}),
});
assert_eq!(
state.transcript,
vec!["tool: ✓ subagents • 1 task • 1 completed".to_string()]
);
}
#[test]
fn failed_bash_tool_result_transcript_shows_status_exit_and_lines_without_command() {
let mut state = MissionControlState::default();
let command = "cargo test super_secret_token=sk-testcredential";
let call = crate::providers::ToolCall {
id: "call_bash_fail".to_string(),
name: "bash".to_string(),
arguments: serde_json::json!({"command": command, "timeout": 10}),
};
let result = crate::tools::ToolResult {
tool_name: "bash".to_string(),
success: false,
content: "error\nmore\n".to_string(),
metadata: serde_json::json!({"exit_code":101,"timed_out":false}),
display: crate::tools::ToolResultDisplay::default(),
};
let summary = crate::output::tool_display_summary(&call, &result);
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(call),
result: Box::new(result),
summary: Box::new(summary),
});
assert_eq!(
state.transcript,
vec!["tool: ✗ bash • exit 101 • 2 lines".to_string()]
);
assert!(!state.transcript[0].contains(command));
assert!(!state.transcript[0].contains("sk-testcredential"));
}
#[test]
fn transcript_rollover_keeps_bookkeeping_bounded_and_tool_behavior_visible() {
let mut state = MissionControlState::default();
for index in 0..500 {
state.push_transcript(format!("info: line {index}"));
}
let call = crate::providers::ToolCall {
id: "call_rollover".to_string(),
name: "read".to_string(),
arguments: serde_json::json!({"path": "src/lib.rs"}),
};
state.apply_output_event(&OutputEvent::ToolStarted {
call: Box::new(call.clone()),
label: "src/lib.rs".to_string(),
});
assert_eq!(state.active_tool_transcripts.len(), 1);
assert_eq!(state.transcript_activity_links.len(), 1);
for index in 0..501 {
state.push_transcript(format!("info: rollover {index}"));
}
assert!(state.active_tool_transcripts.is_empty());
assert!(state.transcript_activity_links.is_empty());
let result = crate::tools::ToolResult {
tool_name: "read".to_string(),
success: true,
content: "file contents".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
};
let summary = crate::output::tool_display_summary(&call, &result);
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(call),
result: Box::new(result),
summary: Box::new(summary),
});
assert_eq!(state.transcript.len(), 500);
assert!(state.transcript.back().unwrap().starts_with("tool: "));
assert!(state.active_tool_transcripts.is_empty());
assert_eq!(state.transcript_activity_links.len(), 1);
}
#[test]
fn transcript_rollover_invalidates_scrollbar_row_estimate() {
let mut state = MissionControlState::default();
for index in 0..500 {
state.push_transcript(format!("info: line {index}"));
}
let _ = state.visible_transcript_lines(80, 6);
assert!(
state
.transcript_cache
.borrow()
.scrollbar_rows
.contains_key(&80)
);
state.push_transcript(format!("assistant: {}", "wrapped ".repeat(80)));
assert!(state.transcript_cache.borrow().scrollbar_rows.is_empty());
}
#[test]
fn thinking_fixed_stream_has_equivalent_transcript_and_linear_append_work() {
fn run(chunks: &[&str]) -> (Vec<String>, usize) {
let mut state = MissionControlState::default();
for chunk in chunks {
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: (*chunk).to_string(),
});
}
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "α line\nmultiline authoritative".to_string(),
});
(
state.transcript.iter().cloned().collect(),
state.thinking_append_work_bytes,
)
}
let text = "α line\nmultiline authoritative";
let coarse = run(&[text]);
let tiny_chunks = text
.char_indices()
.map(|(start, ch)| {
let end = start + ch.len_utf8();
&text[start..end]
})
.chain(std::iter::once(&text[text.len()..]))
.collect::<Vec<_>>();
let tiny = run(&tiny_chunks);
assert_eq!(coarse.0, tiny.0);
assert!(coarse.1 <= text.len());
assert!(tiny.1 <= text.len());
eprintln!(
"tui thinking fixed stream: coarse/tiny append bytes={}/{}",
coarse.1, tiny.1
);
}
#[test]
fn thinking_control_sequences_are_safe_when_split_by_byte() {
for sequence in ["\u{1b}[31m", "\u{1b}]0;hidden title\u{7}"] {
fn run(chunks: &[&str]) -> Vec<String> {
let mut state = MissionControlState::default();
for chunk in chunks {
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta {
text: (*chunk).to_string(),
});
for line in &state.transcript {
assert!(!line.contains('\u{1b}'));
}
}
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: "safe final".to_string(),
});
state.transcript.iter().cloned().collect()
}
let coarse = run(&[&format!("before{sequence}after")]);
let stream = format!("before{sequence}after");
let tiny = stream
.char_indices()
.map(|(start, ch)| &stream[start..start + ch.len_utf8()])
.collect::<Vec<_>>();
let split = run(&tiny);
assert_eq!(coarse, split);
assert!(split.iter().all(|line| !line.contains("hidden title")));
assert!(split.iter().all(|line| !line.contains("31m")));
}
}
#[test]
fn streamed_osc_payload_stays_hidden_across_non_backslash_escape_chunks() {
let source = "safe \u{1b}]hidden\u{1b}Xtail\u{7}visible";
let coarse_chunks = vec!["safe ", "\u{1b}]hidden\u{1b}", "Xtail\u{7}", "visible"];
let byte_chunks = source
.chars()
.map(|character| character.to_string())
.collect::<Vec<_>>();
let stream = |chunks: Vec<String>| {
let mut state = MissionControlState::default();
for chunk in chunks {
state.apply_output_event(&OutputEvent::ThinkingSummaryDelta { text: chunk });
assert!(
state
.transcript
.iter()
.all(|entry| !entry.contains("hidden") && !entry.contains("tail"))
);
}
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: source.to_string(),
});
state.transcript
};
let coarse = stream(
coarse_chunks
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>(),
);
let bytewise = stream(byte_chunks);
assert_eq!(coarse, vec!["thinking: safe visible"]);
assert_eq!(bytewise, coarse);
}