use super::*;
#[test]
fn lifecycle_expansion_and_duplicate_start_are_stable() {
let mut state = MissionControlState::default();
let id = ActivityId::new("tool-1");
let start = ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("bash echo"),
};
state.apply_activity_event(start.clone());
state.expanded.remove(&id);
state.apply_activity_event(start);
assert_eq!(state.roots, vec![id.clone()]);
assert!(!state.expanded.contains(&id));
}
#[test]
fn tool_result_detail_uses_formatted_success_output() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_read",
"read",
serde_json::json!({"path":"file.txt","offset":2,"limit":3}),
);
let id = ActivityId::new(call.id.clone());
state.apply_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("read file.txt"),
});
apply_tool_result(
&mut state,
call,
test_tool_result("read", true, "hello", serde_json::json!({})),
);
let detail = state.selected_activity_text();
assert!(!detail.contains("TOOL START"));
assert!(detail.contains("Status: SUCCESS"), "{detail}");
assert!(detail.contains("\"path\": \"file.txt\""), "{detail}");
assert!(detail.contains("Sanitized Output:"), "{detail}");
assert!(detail.contains("hello"), "{detail}");
assert!(!detail.contains("TOOL END"));
assert_eq!(
state.nodes.get(&id).unwrap().status,
ActivityStatus::Success
);
}
#[test]
fn write_tool_result_detail_includes_destination_path_and_written_data() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_write",
"write",
serde_json::json!({"path":"file.txt","content":"hello\nworld"}),
);
apply_tool_result(
&mut state,
call,
test_tool_result(
"write",
true,
"wrote 11 bytes",
serde_json::json!({"bytes":11}),
),
);
let detail = state.selected_activity_text();
assert!(detail.contains("\"path\": \"file.txt\""), "{detail}");
assert!(
detail.contains("\"content\": \"hello\\nworld\""),
"{detail}"
);
assert!(
detail.contains("Sanitized Output:\nwrote 11 bytes"),
"{detail}"
);
}
#[test]
fn hash_edit_tool_result_detail_includes_display_diff() {
let mut state = MissionControlState::default();
let call = test_tool_call(
"call_hash_edit_diff",
"hash_edit",
serde_json::json!({"input":"[a.txt#ABCD]\nSWAP 1.=1:\n+new"}),
);
let mut result = test_tool_result(
"hash_edit",
true,
"applied 1 hash_edit section(s)",
serde_json::json!({"path":"a.txt","input_bytes":32}),
);
result.display.edit_diff = Some(
"diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1,1 +1,1 @@\n-old\n+new"
.to_string(),
);
apply_tool_result(&mut state, call, result);
let detail = state.selected_activity_text();
assert!(detail.contains("Applied Diff:"), "{detail}");
assert!(detail.contains("diff --git a/a.txt b/a.txt"), "{detail}");
assert!(detail.contains("-old"), "{detail}");
assert!(detail.contains("+new"), "{detail}");
}
#[test]
fn write_tool_result_detail_redacts_sensitive_written_data() {
let mut state = MissionControlState::default();
let secret = "sk-testSecret123456";
let call = test_tool_call(
"call_write",
"write",
serde_json::json!({"path":"file.txt","content":format!("token={secret}")}),
);
apply_tool_result(
&mut state,
call,
test_tool_result("write", true, "wrote", serde_json::json!({"bytes":27})),
);
let detail = state.selected_activity_text();
assert!(
detail.contains("\"content\": \"token=<redacted>\""),
"{detail}"
);
assert!(!detail.contains(secret), "{detail}");
}
#[test]
fn tool_result_detail_uses_formatted_failure_output_and_redacts_secrets() {
let mut state = MissionControlState::default();
let secret = "sk-testSecret123456";
let call = test_tool_call(
"call_bash",
"bash",
serde_json::json!({"command":format!("echo {secret}"),"timeout":9}),
);
apply_tool_result(
&mut state,
call,
test_tool_result(
"bash",
false,
&format!("token={secret}\nboom"),
serde_json::json!({"exit_code":1}),
),
);
let detail = state.selected_activity_text();
assert!(!detail.contains("TOOL START"));
assert!(detail.contains("Status: FAILED"), "{detail}");
assert!(
detail.contains("Sanitized Output:\ntoken=<redacted>\nboom"),
"{detail}"
);
assert!(!detail.contains(secret));
}
#[test]
fn update_context_window_preserves_current_token_estimate() {
let mut state = MissionControlState {
context_usage: Some(ContextUsageState {
current_tokens: 12345,
max_tokens: 128000,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
}),
..Default::default()
};
state.update_context_window(196000);
assert_eq!(
state.context_usage,
Some(ContextUsageState {
current_tokens: 12345,
max_tokens: 196000,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
})
);
}
#[test]
fn unicode_width_drives_wrapping_and_prompt_vertical_movement() {
assert_eq!(prompt::visual_rows("🙂🙂a", 4), 2);
assert_eq!(prompt::visual_rows("e\u{301}e", 2), 1);
let mut state = MissionControlState {
input: "a🙂b".to_string(),
prompt_cursor: "a".len(),
..Default::default()
};
state.move_prompt_cursor_down(2, 3);
assert_eq!(state.prompt_cursor, "a🙂b".len());
assert!(state.input.is_char_boundary(state.prompt_cursor));
}
#[test]
fn assistant_stream_state_boundary_invalidates_cached_card() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "visible assistant text ".repeat(8),
});
reset_project_entry_count_for_test();
let streaming = cached_render_line_text(&state.cached_transcript_lines(40));
let warmed_count = project_entry_count_for_test();
assert!(streaming.contains("Assistant …"), "{streaming}");
state.apply_output_event(&OutputEvent::ThinkingSummaryComplete {
text: String::new(),
});
let finished = cached_render_line_text(&state.cached_transcript_lines(40));
assert!(project_entry_count_for_test() > warmed_count);
assert!(finished.contains("Assistant"), "{finished}");
assert!(!finished.contains("Assistant …"), "{finished}");
}
#[test]
fn assistant_complete_reconciles_dropped_deltas_without_duplication() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Hel".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "Hello world".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "Hello world".to_string(),
});
assert_eq!(state.transcript, vec!["assistant: Hello world".to_string()]);
}
#[test]
fn assistant_deltas_append_incrementally_to_one_active_row() {
let mut state = MissionControlState::default();
for index in 0..80 {
state.apply_output_event(&OutputEvent::AssistantDelta {
text: format!("word{index} "),
});
}
assert_eq!(state.transcript.len(), 1);
assert!(state.transcript[0].starts_with("assistant: word0 word1"));
assert_eq!(state.active_assistant_transcript_index, Some(0));
assert!(state.active_assistant_text.contains("word79"));
}
#[test]
fn assistant_complete_replaces_streamed_partial_text() {
let mut state = MissionControlState::default();
for _ in 0..40 {
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "partial ".to_string(),
});
}
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "authoritative final".to_string(),
});
assert_eq!(state.transcript, vec!["assistant: authoritative final"]);
assert_eq!(state.active_assistant_transcript_index, None);
assert!(state.active_assistant_text.is_empty());
}
#[test]
fn assistant_streaming_redactor_suppresses_split_secret_deltas() {
let mut state = MissionControlState::default();
let secret = "s".repeat(400);
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "api_key=".to_string(),
});
for chunk in secret.as_bytes().chunks(17) {
state.apply_output_event(&OutputEvent::AssistantDelta {
text: std::str::from_utf8(chunk).unwrap().to_string(),
});
}
state.apply_output_event(&OutputEvent::AssistantDelta {
text: " done ".to_string(),
});
let rendered = state.transcript.join("\n");
assert!(!rendered.contains(&secret[..120]), "{rendered}");
}
#[test]
fn assistant_streaming_strips_control_sequences() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: format!(
"{}\u{1b}[31mRED\u{1b}[0m safe {}",
"safe ".repeat(40),
"tail ".repeat(40)
),
});
let rendered = state.transcript.join("\n");
assert!(!rendered.contains('\u{1b}'), "{rendered:?}");
assert!(rendered.contains("RED"), "{rendered}");
}
#[test]
fn assistant_streaming_redactor_resets_after_completion_cancel_and_session_reset() {
let long_visible = "visible ".repeat(40);
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "api_key=".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "done".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: long_visible.clone(),
});
assert!(state.transcript.join("\n").contains("visible"));
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "api_key=".to_string(),
});
state.record_canceled_transcript("stop");
state.apply_output_event(&OutputEvent::AssistantDelta {
text: long_visible.clone(),
});
assert!(state.transcript.join("\n").contains("visible"));
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "api_key=".to_string(),
});
state.reset_for_new_session();
state.apply_output_event(&OutputEvent::AssistantDelta { text: long_visible });
assert!(state.transcript.join("\n").contains("visible"));
}
#[test]
fn prompt_cursor_toggle_only_blinks_when_prompt_focused_and_empty() {
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_cursor_visible: true,
..Default::default()
};
state.toggle_prompt_cursor();
assert!(!state.prompt_cursor_visible);
state.toggle_prompt_cursor();
assert!(state.prompt_cursor_visible);
state.input = "hello".to_string();
state.toggle_prompt_cursor();
assert!(state.prompt_cursor_visible);
state.focus_activity_tree();
state.toggle_prompt_cursor();
assert!(!state.prompt_cursor_visible);
}
#[test]
fn prompt_text_renders_cursor_only_when_focused_and_visible() {
let mut state = MissionControlState {
input: "abcd".to_string(),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_cursor_visible: true,
prompt_cursor: 2,
..Default::default()
};
assert_eq!(state.prompt_text(), "ab█cd");
state.prompt_cursor_visible = false;
assert_eq!(state.prompt_text(), "abcd");
assert_eq!(state.prompt_visual_rows(3), 2);
state.focus_activity_tree();
state.prompt_cursor_visible = true;
assert_eq!(state.prompt_text(), "abcd");
state.focus_prompt();
state.input.clear();
state.prompt_cursor = 0;
state.prompt_cursor_visible = false;
assert_eq!(state.prompt_text(), "");
state.prompt_cursor_visible = true;
assert_eq!(state.prompt_text(), "█");
}
#[test]
fn prompt_cell_mapping_accounts_for_visible_cursor_before_text() {
let state = MissionControlState {
input: "abcd".to_string(),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_cursor_visible: true,
prompt_cursor: 1,
..Default::default()
};
assert_eq!(state.prompt_text(), "a█bcd");
assert_eq!(state.prompt_text_position_for_cell(0, 1, 1, 20).byte, 1);
assert_eq!(state.prompt_text_position_for_cell(0, 2, 1, 20).byte, 1);
assert_eq!(state.prompt_text_position_for_cell(0, 3, 1, 20).byte, 2);
let hidden = MissionControlState {
prompt_cursor_visible: false,
..state
};
assert_eq!(hidden.prompt_text(), "abcd");
assert_eq!(hidden.prompt_text_position_for_cell(0, 2, 1, 20).byte, 2);
}
#[test]
fn prompt_visible_rows_grow_to_five_and_then_tail_scroll() {
let state = MissionControlState {
input: "x".repeat(16),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_cursor_visible: true,
..Default::default()
};
assert_eq!(state.prompt_visual_rows(10), 2);
assert_eq!(state.prompt_visible_rows(10), 2);
assert_eq!(state.prompt_render_scroll(2, 10), 0);
let state = MissionControlState {
input: "x".repeat(64),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_cursor_visible: true,
..Default::default()
};
assert_eq!(state.prompt_visual_rows(10), 7);
assert_eq!(state.prompt_visible_rows(10), MAX_PROMPT_VISIBLE_ROWS);
assert_eq!(state.prompt_render_scroll(MAX_PROMPT_VISIBLE_ROWS, 10), 0);
}
#[test]
fn prompt_left_right_move_by_char_boundaries() {
let mut state = MissionControlState {
input: "aé🙂".to_string(),
prompt_cursor: "aé🙂".len(),
..Default::default()
};
state.move_prompt_cursor_left(3, 10);
assert_eq!(state.prompt_cursor, "aé".len());
state.move_prompt_cursor_left(3, 10);
assert_eq!(state.prompt_cursor, "a".len());
state.move_prompt_cursor_right(3, 10);
assert_eq!(state.prompt_cursor, "aé".len());
assert!(state.input.is_char_boundary(state.prompt_cursor));
}
#[test]
fn prompt_insert_backspace_delete_at_cursor() {
let mut state = MissionControlState {
input: "ac🙂".to_string(),
prompt_cursor: "a".len(),
..Default::default()
};
state.insert_prompt_char('é', 3, 10);
assert_eq!(state.input, "aéc🙂");
assert_eq!(state.prompt_cursor, "aé".len());
state.delete_prompt_char(3, 10);
assert_eq!(state.input, "aé🙂");
state.backspace_prompt_char(3, 10);
assert_eq!(state.input, "a🙂");
assert_eq!(state.prompt_cursor, "a".len());
}
#[test]
fn prompt_visual_rows_count_explicit_newlines_and_trailing_empty_row() {
let state = MissionControlState {
input: "ab\ncd\n".to_string(),
focus_pane: crate::tui::state::TuiFocusPane::ActivityTree,
..Default::default()
};
assert_eq!(state.prompt_visual_rows(10), 3);
assert_eq!(state.prompt_visible_rows(10), 3);
}
#[test]
fn prompt_cursor_auto_reveal_supports_explicit_newlines() {
let mut state = MissionControlState {
input: "a\nb\nc".to_string(),
prompt_cursor: "a\nb\nc".len(),
..Default::default()
};
state.ensure_prompt_cursor_visible(2, 10);
assert_eq!(state.prompt_offset(), 1);
}
#[test]
fn prompt_cursor_auto_reveal_scrolls_above_and_below_fold() {
let mut state = MissionControlState {
input: "x".repeat(20),
prompt_cursor: 0,
..Default::default()
};
state.ensure_prompt_cursor_visible(2, 4);
assert_eq!(state.prompt_offset(), 0);
state.prompt_cursor = 18;
state.ensure_prompt_cursor_visible(2, 4);
assert_eq!(state.prompt_offset(), 3);
}
#[test]
fn prompt_submit_or_clear_resets_display_only_editor_state() {
let mut state = MissionControlState {
input: "hello".to_string(),
prompt_cursor: 2,
prompt_target_column: Some(1),
..Default::default()
};
assert_eq!(state.take_prompt_for_submit(), "hello");
assert_eq!(state.prompt_cursor, 0);
assert_eq!(state.prompt_offset(), 0);
assert_eq!(state.prompt_target_column, None);
state.input = "again".to_string();
state.prompt_cursor = 4;
state.set_scroll_offset(&state.scroll_views.prompt, 1);
state.prompt_target_column = Some(2);
state.clear_prompt_input();
assert!(state.input.is_empty());
assert_eq!(state.prompt_cursor, 0);
assert_eq!(state.prompt_offset(), 0);
assert_eq!(state.prompt_target_column, None);
}
#[test]
fn help_stays_zero_without_overflow() {
let mut state = MissionControlState::default();
let help_text = "short help";
assert_eq!(state.help_overflow(10, 80, help_text), 0);
assert!(!state.scroll_help_down_by(MOUSE_WHEEL_SCROLL_ROWS, 10, 80, help_text));
assert_eq!(state.help_offset(), 0);
assert_eq!(state.help_render_offset(10, 80, help_text), 0);
}
#[test]
fn dropped_pre_tool_delta_complete_preserves_readable_ordering() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "inspect".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "I'll read.".to_string(),
});
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "call_read".to_string(),
name: "read".to_string(),
arguments: serde_json::json!({"path":"file.txt"}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "read".to_string(),
success: true,
content: "file contents".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(crate::output::ToolDisplaySummary {
tool_name: "read".to_string(),
status: crate::output::ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "read file.txt".to_string(),
metadata: vec![],
}),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Done.".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "Done.".to_string(),
});
assert_eq!(
state.transcript,
vec![
"you: inspect".to_string(),
"assistant: I'll read.".to_string(),
"tool: ✓ read file.txt".to_string(),
"assistant: Done.".to_string(),
]
);
}
#[test]
fn interleaved_assistant_tool_assistant_ordering_is_readable() {
let mut state = MissionControlState::default();
state.apply_output_event(&OutputEvent::UserPrompt {
text: "inspect".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "I'll read.".to_string(),
});
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("call_read"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("read file.txt"),
});
state.apply_activity_event(ActivityEvent::Finished {
id: ActivityId::new("call_read"),
status: ActivityStatus::Success,
metadata: None,
});
state.apply_output_event(&OutputEvent::ToolResult {
call: Box::new(crate::providers::ToolCall {
id: "call_read".to_string(),
name: "read".to_string(),
arguments: serde_json::json!({"path":"file.txt"}),
}),
result: Box::new(crate::tools::ToolResult {
tool_name: "read".to_string(),
success: true,
content: "file contents".to_string(),
metadata: serde_json::json!({}),
display: crate::tools::ToolResultDisplay::default(),
}),
summary: Box::new(crate::output::ToolDisplaySummary {
tool_name: "read".to_string(),
status: crate::output::ToolStatus::Success,
unicode_mark: "✓",
ascii_mark: "OK",
label: "read file.txt".to_string(),
metadata: vec![],
}),
});
state.apply_output_event(&OutputEvent::AssistantDelta {
text: "Done.".to_string(),
});
state.apply_output_event(&OutputEvent::AssistantComplete {
text: "Done.".to_string(),
});
assert_eq!(
state.transcript,
vec![
"you: inspect".to_string(),
"assistant: I'll read.".to_string(),
"tool: ✓ read file.txt".to_string(),
"assistant: Done.".to_string(),
]
);
}
#[test]
fn autocomplete_filters_prefix_for_first_slash_token() {
let mut state = MissionControlState {
input: "/n".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
let autocomplete = state.autocomplete.as_ref().unwrap();
assert_eq!(autocomplete.token_start, 0);
assert_eq!(autocomplete.token_end, 2);
assert_eq!(autocomplete.candidates.len(), 1);
assert_eq!(autocomplete.candidates[0].name, "new");
}
#[test]
fn autocomplete_hides_at_first_whitespace_delimiter_but_shows_at_token_eof() {
let candidates = autocomplete_candidates();
let input = "/mo extra";
let mut state = MissionControlState {
input: input.to_string(),
prompt_cursor: "/n".len(),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none());
state.input = "/n".to_string();
state.prompt_cursor = state.input.len();
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
}
#[test]
fn autocomplete_hides_for_non_slash_leading_space_later_token_no_matches_and_dismissed() {
let candidates = autocomplete_candidates();
for input in ["mo", " /mo", "/mo extra"] {
let mut state = MissionControlState {
input: input.to_string(),
prompt_cursor: input.len(),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none(), "input should hide: {input}");
}
let mut state = MissionControlState {
input: "/zz".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none());
let mut state = MissionControlState {
input: "/q".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
state.dismiss_autocomplete_for_current_token();
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none());
}
#[test]
fn autocomplete_accept_replaces_token_byte_safely_and_preserves_surrounding_text() {
let mut state = MissionControlState {
input: "/ne café".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert!(state.accept_autocomplete(1, 80));
assert_eq!(state.input, "/new café");
assert_eq!(state.prompt_cursor, "/new".len());
assert!(state.autocomplete.is_none());
}
#[test]
fn autocomplete_clears_on_prompt_clear_and_submit() {
let candidates = autocomplete_candidates();
let mut state = MissionControlState {
input: "/n".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
state.clear_prompt_input();
assert!(state.autocomplete.is_none());
state.input = "/n".to_string();
state.prompt_cursor = 3;
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
assert_eq!(state.take_prompt_for_submit(), "/n");
assert!(state.autocomplete.is_none());
}
#[test]
fn autocomplete_fuzzy_matches_file_tokens_anywhere_in_prompt() {
let mut state = MissionControlState {
input: "please inspect @tuiac".to_string(),
prompt_cursor: "please inspect @tuiac".len(),
..Default::default()
};
state.recompute_autocomplete(&file_autocomplete_candidates());
let autocomplete = state.autocomplete.as_ref().unwrap();
assert_eq!(autocomplete.token_start, "please inspect ".len());
assert_eq!(autocomplete.token_end, state.input.len());
assert_eq!(
autocomplete.candidates[0].name,
"src/tui/input/autocomplete.rs"
);
assert_eq!(autocomplete.candidates[0].kind, AutocompleteKind::FileTag);
}
#[test]
fn autocomplete_file_token_accepts_with_trailing_space_only_at_end() {
let candidates = file_autocomplete_candidates();
let mut state = MissionControlState {
input: "see @read".to_string(),
prompt_cursor: "see @read".len(),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.accept_autocomplete(1, 80));
assert_eq!(state.input, "see @README.md ");
assert_eq!(state.prompt_cursor, "see @README.md ".len());
let mut state = MissionControlState {
input: "see @read, please".to_string(),
prompt_cursor: "see @read".len(),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.accept_autocomplete(1, 80));
assert_eq!(state.input, "see @README.md, please");
assert_eq!(state.prompt_cursor, "see @README.md".len());
}
#[test]
fn autocomplete_file_token_terminators_preserve_slash_dot_underscore_and_dash() {
let candidates = vec![AutocompleteCandidate::file_tag("src/foo_bar-baz.rs")];
let input = "open (@src/foo_bar-baz.rs)";
let mut state = MissionControlState {
input: input.to_string(),
prompt_cursor: "open (@src/foo_bar".len(),
..Default::default()
};
state.recompute_autocomplete(&candidates);
let autocomplete = state.autocomplete.as_ref().unwrap();
assert_eq!(autocomplete.token_start, "open (".len());
assert_eq!(autocomplete.token_end, input.len() - 1);
assert_eq!(autocomplete.candidates[0].name, "src/foo_bar-baz.rs");
}
#[test]
fn thinking_level_cycle_uses_exact_available_levels() {
let mut state = MissionControlState::default();
state.refresh_thinking_levels(
crate::thinking::ThinkingLevel::Default,
vec![
crate::thinking::ThinkingLevel::Default,
crate::thinking::ThinkingLevel::Low,
crate::thinking::ThinkingLevel::Medium,
crate::thinking::ThinkingLevel::High,
crate::thinking::ThinkingLevel::XHigh,
],
);
assert_eq!(
state.next_thinking_level(),
Some(crate::thinking::ThinkingLevel::Low)
);
state.refresh_thinking_levels(
crate::thinking::ThinkingLevel::High,
vec![
crate::thinking::ThinkingLevel::Default,
crate::thinking::ThinkingLevel::High,
crate::thinking::ThinkingLevel::Max,
],
);
assert_eq!(
state.next_thinking_level(),
Some(crate::thinking::ThinkingLevel::Max)
);
state.refresh_thinking_levels(
crate::thinking::ThinkingLevel::Medium,
vec![
crate::thinking::ThinkingLevel::Default,
crate::thinking::ThinkingLevel::High,
crate::thinking::ThinkingLevel::Max,
],
);
assert_eq!(
state.effective_thinking_level,
crate::thinking::ThinkingLevel::Default
);
state.refresh_thinking_levels(
crate::thinking::ThinkingLevel::High,
vec![crate::thinking::ThinkingLevel::Default],
);
assert_eq!(state.next_thinking_level(), None);
}
#[test]
fn tool_activity_detail_matrix_preserves_structured_contract_and_bounds() {
let cases = [
("read", "read", serde_json::json!({"path":"builtin.txt"})),
("shell", "shell", serde_json::json!({"cmd":"printf alias"})),
(
"mcp__server__lookup",
"mcp__server__lookup",
serde_json::json!({"query":"dynamic"}),
),
(
"dynamic_tool",
"dynamic_tool",
serde_json::json!({"input":"dynamic"}),
),
];
let output = format!(
"{}\n{}",
"line".repeat(16 * 1024),
(0..201)
.map(|i| format!("row-{i}"))
.collect::<Vec<_>>()
.join("\n")
);
for (index, (name, label, params)) in cases.into_iter().enumerate() {
let mut state = MissionControlState::default();
let call = test_tool_call(format!("matrix-{index}").as_str(), name, params);
apply_tool_result(
&mut state,
call,
test_tool_result(name, true, &output, serde_json::json!({"kind": name})),
);
let detail = state.selected_activity_text();
assert!(
detail.contains(&format!("Tool • {name} • {label}")),
"{detail}"
);
assert!(detail.contains("\"metadata\":"), "{detail}");
assert!(detail.contains("\"params\":"), "{detail}");
assert!(detail.contains("row-200"), "{detail}");
assert!(detail.contains("line"), "{detail}");
assert!(!detail.contains("activity preview truncated"), "{detail}");
}
}
#[test]
fn empty_tool_activity_detail_has_explicit_empty_output() {
let mut state = MissionControlState::default();
apply_tool_result(
&mut state,
test_tool_call("empty", "dynamic_tool", serde_json::json!({"x":1})),
test_tool_result("dynamic_tool", true, "", serde_json::json!({})),
);
let detail = state.selected_activity_text();
assert!(detail.contains("Sanitized Output:"), "{detail}");
}