use super::*;
#[test]
fn activity_fast_observations_reject_stale_events_and_warn_once() {
fn event(
run_order: Option<u64>,
request_sequence: u64,
outcome: crate::fast::FastOutcome,
) -> ActivityEvent {
ActivityEvent::FastObservation {
provider_id: "provider".to_string(),
model: "model".to_string(),
requested_service_tier: "priority".to_string(),
outcome,
request_sequence,
run_order,
}
}
let mut state = MissionControlState::default();
state.apply_activity_event(event(
Some(2),
1,
crate::fast::FastOutcome::Different("default".to_string()),
));
let warning = state.status.clone();
state.apply_activity_event(event(
Some(1),
u64::MAX,
crate::fast::FastOutcome::Unconfirmed,
));
state.apply_activity_event(event(
Some(2),
1,
crate::fast::FastOutcome::Different("default".to_string()),
));
state.apply_activity_event(event(Some(2), 2, crate::fast::FastOutcome::Confirmed));
assert_eq!(state.status, warning);
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.run_order, Some(2));
assert_eq!(stored.request_sequence, 2);
assert_eq!(stored.outcome, crate::fast::FastOutcome::Confirmed);
}
#[test]
fn fast_warning_dedupe_includes_tier_and_outcome_transitions() {
fn event(
requested_service_tier: &str,
run_order: u64,
outcome: crate::fast::FastOutcome,
) -> ActivityEvent {
ActivityEvent::FastObservation {
provider_id: "provider".to_string(),
model: "model".to_string(),
requested_service_tier: requested_service_tier.to_string(),
outcome,
request_sequence: run_order,
run_order: Some(run_order),
}
}
let mut state = MissionControlState::default();
state.apply_activity_event(event(
"priority",
1,
crate::fast::FastOutcome::Different("default".to_string()),
));
state.apply_activity_event(event("priority", 2, crate::fast::FastOutcome::Unconfirmed));
state.apply_activity_event(event(
"standard",
3,
crate::fast::FastOutcome::Different("default".to_string()),
));
state.apply_activity_event(event(
"priority",
4,
crate::fast::FastOutcome::Different("default".to_string()),
));
let warning_count = state
.transcript
.iter()
.filter(|line| line.starts_with("warning: "))
.count();
assert_eq!(warning_count, 3);
}
#[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 reset_context_for_model_switch_hides_unknown_usage() {
let mut state = MissionControlState {
context_usage: Some(ContextUsageState {
current_tokens: 12_345,
max_tokens: 128_000,
reasoning_tokens: Some(9),
source: ContextUsageSource::ProviderExact,
request_sequence: 7,
}),
..Default::default()
};
state.reset_context_for_model_switch();
assert_eq!(state.context_usage, None);
}
#[test]
fn unicode_width_drives_wrapping_and_prompt_vertical_movement() {
assert_eq!(PromptEditor::new("界界a", 0).visual_rows(4), 2);
assert_eq!(PromptEditor::new("e\u{301}e", 0).visual_rows(2), 1);
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("a界b", "a".len()),
..Default::default()
};
let area = ratatui::layout::Rect::new(0, 0, 8, 20);
let mut terminal =
ratatui::Terminal::new(ratatui::backend::TestBackend::new(area.width, area.height))
.unwrap();
terminal
.draw(|frame| crate::tui::render::draw(frame, &state))
.unwrap();
state.move_prompt_cursor_down(2, 3);
assert_eq!(state.prompt_cursor(), "a界b".len());
assert!(
state
.prompt_plain_text()
.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("Agent …"), "{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("Agent"), "{finished}");
assert!(!finished.contains("Agent …"), "{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,
..Default::default()
};
state.toggle_prompt_cursor();
assert!(!state.prompt_cursor_visible());
state.toggle_prompt_cursor();
assert!(state.prompt_cursor_visible());
state.set_prompt_text("hello", "hello".len());
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_and_cursor_visibility_stay_in_the_editor() {
let mut state = MissionControlState {
prompt_editor: PromptEditor::new_with_visibility("abcd", 2, true),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
assert_eq!(state.prompt_plain_text(), "abcd");
assert_eq!(state.prompt_cursor(), 2);
assert!(state.prompt_cursor_visible());
state.set_prompt_cursor_visible(false);
assert!(!state.prompt_cursor_visible());
assert_eq!(state.prompt_plain_text(), "abcd");
state.focus_activity_tree();
assert!(!state.prompt_cursor_visible());
state.focus_prompt();
assert!(state.prompt_cursor_visible());
}
#[test]
fn prompt_visible_rows_are_capped_without_changing_visual_measurement() {
let state = MissionControlState {
prompt_editor: PromptEditor::new_with_visibility(&"x".repeat(16), 0, true),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
assert_eq!(state.prompt_visual_rows(10), 2);
assert_eq!(state.prompt_visible_rows(10), 2);
let state = MissionControlState {
prompt_editor: PromptEditor::new_with_visibility(&"x".repeat(64), 0, true),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
assert_eq!(state.prompt_visual_rows(10), 7);
assert_eq!(state.prompt_visible_rows(10), MAX_PROMPT_VISIBLE_ROWS);
}
#[test]
fn prompt_submit_restore_preserves_the_textarea_snapshot() {
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("hello\nworld", 3),
..Default::default()
};
state.prompt_editor.start_selection_at(0);
state.prompt_editor.update_selection_at(5);
assert_eq!(state.take_prompt_for_submit(), "hello\nworld");
assert!(state.prompt_plain_text().is_empty());
assert_eq!(state.prompt_cursor(), 0);
state.restore_rejected_prompt("ignored fallback".to_string());
assert_eq!(state.prompt_plain_text(), "hello\nworld");
assert_eq!(state.prompt_cursor(), 5);
assert_eq!(
state.prompt_editor.selection_text().as_deref(),
Some("hello")
);
}
#[test]
fn prompt_left_right_move_by_char_boundaries() {
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("aé界", "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
.prompt_plain_text()
.is_char_boundary(state.prompt_cursor())
);
}
#[test]
fn prompt_insert_backspace_delete_at_cursor() {
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("ac界", "a".len()),
..Default::default()
};
state.insert_prompt_char('é', 3, 10);
assert_eq!(state.prompt_plain_text(), "aéc界");
assert_eq!(state.prompt_cursor(), "aé".len());
state.delete_prompt_char(3, 10);
assert_eq!(state.prompt_plain_text(), "aé界");
state.backspace_prompt_char(3, 10);
assert_eq!(state.prompt_plain_text(), "a界");
assert_eq!(state.prompt_cursor(), "a".len());
}
#[test]
fn prompt_visual_rows_count_explicit_newlines_and_trailing_empty_row() {
let state = MissionControlState {
prompt_editor: PromptEditor::new("ab\ncd\n", 0),
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 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 {
changed_paths: Vec::new(),
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 {
changed_paths: Vec::new(),
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 {
prompt_editor: PromptEditor::new("/n", 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 {
prompt_editor: PromptEditor::new(input, "/n".len()),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none());
state.set_prompt_text("/n", "/n".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 {
prompt_editor: PromptEditor::new(input, input.len()),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none(), "input should hide: {input}");
}
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("/zz", 2),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete.is_none());
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("/q", 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 {
prompt_editor: PromptEditor::new("/ne café", 2),
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert_eq!(state.accept_autocomplete(1, 80), PromptEditResult::Changed);
assert_eq!(state.prompt_plain_text(), "/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 {
prompt_editor: PromptEditor::new("/n", 2),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
state.clear_prompt_input();
assert!(state.autocomplete.is_none());
state.set_prompt_text("/n", 3);
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
assert_eq!(state.take_prompt_for_submit(), "/n");
assert!(state.autocomplete.is_none());
}
#[test]
fn oversized_prompt_edit_sets_exact_status_without_mutating_editor() {
let text = "x".repeat(MAX_PROMPT_BYTES);
let mut state = MissionControlState {
prompt_editor: PromptEditor::new(&text, text.len()),
focus_pane: TuiFocusPane::Prompt,
status: "previous status".to_string(),
..Default::default()
};
let before_cursor = state.prompt_cursor();
assert_eq!(
state.insert_prompt_str("y", 1, 80),
PromptEditResult::RejectedTooLarge
);
assert_eq!(state.prompt_plain_text(), text);
assert_eq!(state.prompt_cursor(), before_cursor);
assert_eq!(state.status, PROMPT_TOO_LARGE_STATUS);
}
#[test]
fn rejected_autocomplete_acceptance_preserves_suggestions_and_editor() {
let text = format!("/n {}", "x".repeat(MAX_PROMPT_BYTES - 3));
let mut state = MissionControlState {
prompt_editor: PromptEditor::new(&text, 1),
focus_pane: TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
let autocomplete_before = state.autocomplete.clone();
let cursor_before = state.prompt_cursor();
assert_eq!(
state.accept_autocomplete(1, 80),
PromptEditResult::RejectedTooLarge
);
assert_eq!(state.prompt_plain_text(), text);
assert_eq!(state.prompt_cursor(), cursor_before);
assert_eq!(state.autocomplete, autocomplete_before);
assert_eq!(state.status, PROMPT_TOO_LARGE_STATUS);
}
#[test]
fn autocomplete_fuzzy_matches_file_tokens_anywhere_in_prompt() {
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("please inspect @tuiac", "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.prompt_plain_text().len());
assert_eq!(autocomplete.candidates[0].name, "src/tui/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 {
prompt_editor: PromptEditor::new("see @read", "see @read".len()),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert_eq!(state.accept_autocomplete(1, 80), PromptEditResult::Changed);
assert_eq!(state.prompt_plain_text(), "see @README.md ");
assert_eq!(state.prompt_cursor(), "see @README.md ".len());
let mut state = MissionControlState {
prompt_editor: PromptEditor::new("see @read, please", "see @read".len()),
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert_eq!(state.accept_autocomplete(1, 80), PromptEditResult::Changed);
assert_eq!(state.prompt_plain_text(), "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 {
prompt_editor: PromptEditor::new(input, "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}");
}
#[test]
fn fast_display_state_resolves_all_states() {
let standard = crate::fast::FastRequestState::Standard;
let fast = crate::fast::FastRequestState::Fast {
service_tier: "priority".to_string(),
};
let confirmed = crate::fast::FastObservation {
run_order: Some(1),
request_sequence: 1,
requested_service_tier: "priority".to_string(),
outcome: crate::fast::FastOutcome::Confirmed,
};
let different = crate::fast::FastObservation {
run_order: Some(2),
request_sequence: 2,
requested_service_tier: "priority".to_string(),
outcome: crate::fast::FastOutcome::Different("default".to_string()),
};
let unconfirmed = crate::fast::FastObservation {
run_order: Some(3),
request_sequence: 3,
requested_service_tier: "priority".to_string(),
outcome: crate::fast::FastOutcome::Unconfirmed,
};
assert_eq!(
FastDisplayState::resolve(false, &fast, None),
FastDisplayState::Off
);
assert_eq!(
FastDisplayState::resolve(true, &standard, None),
FastDisplayState::Unavailable
);
assert_eq!(
FastDisplayState::resolve(true, &fast, None),
FastDisplayState::WillRequest
);
assert_eq!(
FastDisplayState::resolve(true, &fast, Some(&confirmed)),
FastDisplayState::Confirmed
);
assert_eq!(
FastDisplayState::resolve(true, &fast, Some(&different)),
FastDisplayState::Different("default".to_string())
);
assert_eq!(
FastDisplayState::resolve(true, &fast, Some(&unconfirmed)),
FastDisplayState::Unconfirmed
);
}
#[test]
fn fast_display_state_ignores_observation_from_previous_capability_tier() {
let observation = crate::fast::FastObservation {
run_order: Some(1),
request_sequence: 1,
requested_service_tier: "priority".to_string(),
outcome: crate::fast::FastOutcome::Confirmed,
};
let current_capability = crate::fast::FastRequestState::Fast {
service_tier: "standard".to_string(),
};
assert_eq!(
FastDisplayState::resolve(true, ¤t_capability, Some(&observation)),
FastDisplayState::WillRequest
);
}
#[test]
fn selected_fast_observation_follows_provider_and_model_key() {
let mut state = MissionControlState {
provider: "provider-a".to_string(),
model: "model-a".to_string(),
..Default::default()
};
let confirmed = crate::fast::FastOutcome::Confirmed;
let different = crate::fast::FastOutcome::Different("default".to_string());
let unconfirmed = crate::fast::FastOutcome::Unconfirmed;
let capability = crate::fast::FastRequestState::Fast {
service_tier: "priority".to_string(),
};
assert!(state.accept_fast_observation(
"provider-a",
"model-a",
"priority",
&confirmed,
1,
Some(1),
));
assert!(state.accept_fast_observation(
"provider-a",
"model-b",
"priority",
&different,
1,
Some(1),
));
assert!(state.accept_fast_observation(
"provider-b",
"model-b",
"priority",
&unconfirmed,
1,
Some(1),
));
assert_eq!(state.selected_fast_observation_outcome(), Some(&confirmed));
assert_eq!(
state.fast_display_state(true, &capability),
FastDisplayState::Confirmed
);
state.model = "model-b".to_string();
assert_eq!(state.selected_fast_observation_outcome(), Some(&different));
assert_eq!(
state.fast_display_state(true, &capability),
FastDisplayState::Different("default".to_string())
);
state.provider = "provider-b".to_string();
assert_eq!(
state.selected_fast_observation_outcome(),
Some(&unconfirmed)
);
assert_eq!(
state.fast_display_state(true, &capability),
FastDisplayState::Unconfirmed
);
state.provider = "provider-c".to_string();
assert!(state.selected_fast_observation_outcome().is_none());
}
#[test]
fn clearing_fast_observations_removes_selected_state_and_warning_keys() {
let mut state = MissionControlState {
provider: "provider".to_string(),
model: "model".to_string(),
..Default::default()
};
let outcome = crate::fast::FastOutcome::Different("default".to_string());
assert!(state.accept_fast_observation("provider", "model", "priority", &outcome, 1, Some(1),));
assert!(state.selected_fast_observation().is_some());
assert!(!state.fast_warning_keys.is_empty());
state.clear_fast_observations();
assert!(state.selected_fast_observation().is_none());
assert!(state.fast_warning_keys.is_empty());
}
#[test]
fn refresh_fast_mode_state_stores_controller_effectiveness() {
let mut state = MissionControlState::default();
let standard = crate::fast::FastRequestState::Standard;
let fast = crate::fast::FastRequestState::Fast {
service_tier: "priority".to_string(),
};
state.refresh_fast_mode_state(true, &standard);
assert!(state.fast_mode_enabled);
assert!(!state.fast_mode_effective);
state.refresh_fast_mode_state(true, &fast);
assert!(state.fast_mode_enabled);
assert!(state.fast_mode_effective);
state.refresh_fast_mode_state(false, &fast);
assert!(!state.fast_mode_enabled);
assert!(!state.fast_mode_effective);
}
#[test]
fn fast_mode_rainbow_gate_wraps_phase_and_preserves_it_when_ineligible() {
let mut state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-5.5".to_string(),
provider_ready: true,
color_enabled: true,
fast_mode_rainbow_phase: 6,
..Default::default()
};
let fast = crate::fast::FastRequestState::Fast {
service_tier: "priority".to_string(),
};
state.refresh_fast_mode_state(true, &fast);
assert!(state.fast_mode_rainbow_active());
assert!(state.advance_fast_mode_rainbow_phase());
assert_eq!(state.fast_mode_rainbow_phase, 0);
state.color_enabled = false;
assert!(!state.fast_mode_rainbow_active());
assert!(!state.advance_fast_mode_rainbow_phase());
assert_eq!(state.fast_mode_rainbow_phase, 0);
state.color_enabled = true;
state.provider_ready = false;
assert!(!state.fast_mode_rainbow_active());
state.provider_ready = true;
state.provider.clear();
state.model.clear();
assert!(!state.fast_mode_rainbow_active());
}
fn fast_observation_event(
provider: &str,
model: &str,
run_order: Option<u64>,
request_sequence: u64,
requested_service_tier: &str,
outcome: crate::fast::FastOutcome,
) -> OutputEvent {
OutputEvent::FastObservation {
provider_id: provider.to_string(),
model: model.to_string(),
requested_service_tier: requested_service_tier.to_string(),
outcome,
request_sequence,
run_order,
}
}
#[test]
fn fast_observation_rejects_an_older_run() {
let mut state = MissionControlState::default();
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(2),
1,
"priority",
crate::fast::FastOutcome::Different("default".to_string()),
));
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(1),
u64::MAX,
"priority",
crate::fast::FastOutcome::Unconfirmed,
));
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.run_order, Some(2));
assert_eq!(stored.request_sequence, 1);
assert_eq!(stored.accept_sequence, 1);
assert_eq!(
stored.outcome,
crate::fast::FastOutcome::Different("default".to_string())
);
}
#[test]
fn fast_observation_orders_events_by_parent_arrival_within_a_run() {
let mut state = MissionControlState::default();
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(4),
1,
"priority",
crate::fast::FastOutcome::Different("first".to_string()),
));
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(4),
1,
"priority",
crate::fast::FastOutcome::Unconfirmed,
));
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.request_sequence, 1);
assert_eq!(stored.accept_sequence, 2);
assert_eq!(stored.outcome, crate::fast::FastOutcome::Unconfirmed);
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(4),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.accept_sequence, 3);
assert_eq!(stored.outcome, crate::fast::FastOutcome::Confirmed);
}
#[test]
fn fast_observation_order_handles_unknown_runs_conservatively() {
let mut state = MissionControlState::default();
state.apply_output_event(&fast_observation_event(
"provider",
"model",
None,
1,
"priority",
crate::fast::FastOutcome::Different("unknown-first".to_string()),
));
state.apply_output_event(&fast_observation_event(
"provider",
"model",
None,
1,
"priority",
crate::fast::FastOutcome::Different("unknown-later".to_string()),
));
assert_eq!(
state.fast_observation("provider", "model").unwrap().outcome,
crate::fast::FastOutcome::Different("unknown-later".to_string())
);
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(1),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.run_order, Some(1));
assert_eq!(stored.accept_sequence, 3);
state.apply_output_event(&fast_observation_event(
"provider",
"model",
None,
u64::MAX,
"priority",
crate::fast::FastOutcome::Unconfirmed,
));
let stored = state.fast_observation("provider", "model").unwrap();
assert_eq!(stored.run_order, Some(1));
assert_eq!(stored.accept_sequence, 3);
assert_eq!(stored.outcome, crate::fast::FastOutcome::Confirmed);
}
#[test]
fn fast_observations_are_isolated_per_model() {
let mut state = MissionControlState::default();
state.apply_output_event(&fast_observation_event(
"provider",
"model-a",
Some(5),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
state.apply_output_event(&fast_observation_event(
"provider",
"model-b",
Some(1),
1,
"priority",
crate::fast::FastOutcome::Unconfirmed,
));
assert_eq!(
state
.fast_observation("provider", "model-a")
.unwrap()
.run_order,
Some(5)
);
assert_eq!(
state
.fast_observation("provider", "model-b")
.unwrap()
.run_order,
Some(1)
);
}
#[test]
fn reset_for_new_session_clears_fast_observations() {
let mut state = MissionControlState::default();
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(1),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
state.reset_for_new_session();
assert!(state.fast_observation("provider", "model").is_none());
assert_eq!(state.fast_observation_accept_sequence, 0);
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(2),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
assert_eq!(
state
.fast_observation("provider", "model")
.unwrap()
.accept_sequence,
1
);
}
#[test]
fn different_and_unconfirmed_fast_observations_set_a_warning_status() {
for outcome in [
crate::fast::FastOutcome::Different("default".to_string()),
crate::fast::FastOutcome::Unconfirmed,
] {
let mut state = MissionControlState::default();
let capability = crate::fast::FastRequestState::Fast {
service_tier: "priority".to_string(),
};
let observation = crate::fast::FastObservation {
requested_service_tier: "priority".to_string(),
outcome: outcome.clone(),
request_sequence: 1,
run_order: Some(1),
};
let expected = crate::fast::fast_status_message(
true,
"provider",
"model",
&capability,
Some(&observation),
);
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(1),
1,
"priority",
outcome,
));
assert_eq!(state.status, expected);
}
}
#[test]
fn confirmed_fast_observation_does_not_change_status() {
let mut state = MissionControlState {
status: "existing status".to_string(),
..Default::default()
};
state.apply_output_event(&fast_observation_event(
"provider",
"model",
Some(1),
1,
"priority",
crate::fast::FastOutcome::Confirmed,
));
assert_eq!(state.status, "existing status");
}