use super::super::*;
use super::support::*;
use std::time::Instant;
#[test]
fn immutable_key_classifier_emits_prompt_edit_without_mutating_state() {
let state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_editor: crate::tui::state::PromptEditor::new("ab", 2),
..Default::default()
};
let before = (
state.prompt_plain_text(),
state.prompt_cursor(),
state.prompt_content_revision(),
state.focus_pane,
);
let command = classify_key_at(
key(KeyCode::Char('x')),
&state,
KeyViewports {
prompt: Some(PromptViewport {
visible_rows: 3,
wrap_width: 20,
}),
..KeyViewports::default()
},
&[],
Instant::now(),
);
let (_, display) = command.into_parts();
let Some(DisplayCommand::Batch(commands)) = display else {
panic!("expected standalone-control prefix and prompt command");
};
assert!(matches!(
commands.last(),
Some(DisplayCommand::Prompt(PromptCommand::Edit {
operation: PromptEditCommand::InsertChar('x'),
refresh_autocomplete: true,
..
}))
));
assert_eq!(
(
state.prompt_plain_text(),
state.prompt_cursor(),
state.prompt_content_revision(),
state.focus_pane,
),
before
);
}
#[test]
fn immutable_paste_classifier_emits_insert_without_mutating_state() {
let state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_editor: crate::tui::state::PromptEditor::new("/", 1),
..Default::default()
};
let before = (state.prompt_plain_text(), state.prompt_cursor());
let command = classify_paste_with_all_viewports(
"new",
&state,
KeyViewports {
prompt: Some(PromptViewport {
visible_rows: 2,
wrap_width: 16,
}),
..KeyViewports::default()
},
&[],
);
assert!(matches!(
command,
InputCommand::Display(DisplayCommand::Prompt(PromptCommand::InsertText {
text,
refresh_autocomplete: true,
viewport: PromptViewport {
visible_rows: 2,
wrap_width: 16,
},
})) if text == "new"
));
assert_eq!((state.prompt_plain_text(), state.prompt_cursor()), before);
}
#[test]
fn immutable_modal_classifier_emits_close_without_mutating_state() {
let mut state = MissionControlState::default();
state.open_system_prompt_modal("system prompt".to_string());
let before = (
state.system_prompt_modal_visible(),
state.focus_pane,
state.status.clone(),
);
let command = classify_key_at(
key(KeyCode::Esc),
&state,
KeyViewports::default(),
&[],
Instant::now(),
);
let (_, display) = command.into_parts();
let Some(DisplayCommand::Batch(commands)) = display else {
panic!("expected standalone-control prefix and modal command");
};
assert!(matches!(
commands.last(),
Some(DisplayCommand::Modal(ModalCommand::CloseSystemPrompt))
));
assert_eq!(
(
state.system_prompt_modal_visible(),
state.focus_pane,
state.status,
),
before
);
}
#[test]
fn immutable_mouse_classifier_emits_click_without_mutating_state() {
let area = ratatui::layout::Rect::new(0, 0, 100, 30);
let state = state_with_activity();
let activity_area = crate::tui::render::layout_for(area, &state)
.pane_area(crate::tui::render::TuiPane::ActivityTree)
.expect("activity pane");
let text_area = crate::tui::render::content_text_area_for_pane(
crate::tui::render::TuiPane::ActivityTree,
activity_area,
);
let mouse = crossterm::event::MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: text_area.x,
row: text_area.y,
modifiers: KeyModifiers::NONE,
};
let before = (state.selected, state.focus_pane, state.prompt_plain_text());
let command = crate::tui::input::mouse::classify_mouse_event(mouse, area, &state, 3)
.expect("activity click command");
assert!(matches!(
command,
DisplayCommand::Mouse(MouseCommand::ClickActivityTree { .. })
));
assert_eq!(
(state.selected, state.focus_pane, state.prompt_plain_text()),
before
);
}
#[test]
fn production_classifier_and_reducer_apply_prompt_edit_matrix() {
let viewport = PromptViewport {
visible_rows: 3,
wrap_width: 20,
};
let cases = [
(key(KeyCode::Char('x')), "ab", 2, "abx", 3),
(key(KeyCode::Backspace), "ab", 2, "a", 1),
(key(KeyCode::Delete), "ab", 1, "a", 1),
(
modified_key(KeyCode::Enter, KeyModifiers::SHIFT),
"ab",
1,
"a\nb",
2,
),
];
for (key, initial, cursor, expected, expected_cursor) in cases {
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_editor: crate::tui::state::PromptEditor::new(initial, cursor),
..Default::default()
};
let command = classify_key_with_all_viewports(
key,
&state,
KeyViewports {
prompt: Some(viewport),
..KeyViewports::default()
},
&[],
);
let (action, display) = command.into_parts();
assert_eq!(action, InputAction::None);
let (changed, copy) = crate::tui::controller::apply_display_command_for_test(
display.expect("classified prompt edit"),
&mut state,
&[],
);
assert!(changed, "expected prompt edit for {key:?}");
assert_eq!(copy, None);
assert_eq!(state.prompt_plain_text(), expected);
assert_eq!(state.prompt_cursor(), expected_cursor);
}
}
#[test]
fn subagent_viewer_keys_route_to_bounded_modal_commands() {
let mut state = MissionControlState::default();
let batch = ActivityId::new("input-viewer-batch");
state.apply_activity_event(ActivityEvent::Started {
id: batch.clone(),
parent_id: None,
kind: ActivityKind::SubagentBatch,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("subagents"),
});
for task in ["g1", "g2"] {
state.apply_activity_event(ActivityEvent::Started {
id: batch.child(task),
parent_id: Some(batch.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(task),
});
}
state.open_subagent_viewer(batch, ActivityId::new("input-viewer-batch/g1"));
let viewport = SystemPromptViewport {
visible_rows: 5,
wrap_width: 40,
};
let cases = [
(KeyCode::Tab, ModalCommand::SubagentViewerSelectNext),
(KeyCode::Right, ModalCommand::SubagentViewerSelectNext),
(KeyCode::BackTab, ModalCommand::SubagentViewerSelectPrevious),
(KeyCode::Left, ModalCommand::SubagentViewerSelectPrevious),
(KeyCode::Home, ModalCommand::SubagentViewerScrollHome),
];
for (code, expected) in cases {
let (_, display) = classify_key_at(
key(code),
&state,
KeyViewports {
subagent_viewer: Some(viewport),
..KeyViewports::default()
},
&[],
Instant::now(),
)
.into_parts();
let display = display.expect("viewer display command");
let actual = match display {
DisplayCommand::Batch(mut commands) => commands.pop().expect("viewer command"),
display => display,
};
assert_eq!(actual, DisplayCommand::Modal(expected));
}
for (code, down, rows) in [
(KeyCode::Up, false, 1),
(KeyCode::Down, true, 1),
(KeyCode::PageUp, false, 5),
(KeyCode::PageDown, true, 5),
] {
let (_, display) = classify_key_at(
key(code),
&state,
KeyViewports {
subagent_viewer: Some(viewport),
..KeyViewports::default()
},
&[],
Instant::now(),
)
.into_parts();
let display = display.expect("viewer scroll command");
let actual = match display {
DisplayCommand::Batch(mut commands) => commands.pop().expect("viewer command"),
display => display,
};
assert_eq!(
actual,
DisplayCommand::Scroll(ScrollCommand {
target: ScrollTarget::SubagentViewer,
down,
rows,
visible_rows: 5,
wrap_width: 40,
})
);
}
let (_, display) = classify_key_at(
key(KeyCode::End),
&state,
KeyViewports {
subagent_viewer: Some(viewport),
..KeyViewports::default()
},
&[],
Instant::now(),
)
.into_parts();
assert!(matches!(
display,
Some(DisplayCommand::Batch(commands))
if matches!(commands.last(), Some(DisplayCommand::Modal(ModalCommand::SubagentViewerScrollEnd { viewport: actual })) if *actual == viewport)
));
}
#[test]
fn plain_tab_classifier_emits_next_primary_pane_command() {
let state = MissionControlState::default();
let (action, display) = classify_key_at(
key(KeyCode::Tab),
&state,
KeyViewports::default(),
&[],
Instant::now(),
)
.into_parts();
assert_eq!(action, InputAction::None);
let Some(DisplayCommand::Batch(commands)) = display else {
panic!("expected standalone-control prefix and tab command");
};
assert!(matches!(
commands.last(),
Some(DisplayCommand::Focus(FocusCommand::NextPrimaryPane))
));
assert!(state.is_prompt_focused());
}