Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,498 @@
|
||||
use super::SlashCommandEntryState;
|
||||
use crate::report_if_error;
|
||||
use crate::search::slash_command_menu::static_commands::commands;
|
||||
use crate::settings::AISettings;
|
||||
use crate::terminal::input::tests::{add_window_with_bootstrapped_terminal, initialize_app};
|
||||
use settings::Setting as _;
|
||||
use warpui::{App, SingletonEntity as _};
|
||||
|
||||
#[test]
|
||||
fn test_parse_slash_command_handles_argument_rules() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
let slash_command_data_source =
|
||||
input.read(&app, |input, _| input.slash_command_data_source.clone());
|
||||
|
||||
slash_command_data_source.read(&app, |data_source, _| {
|
||||
let no_argument_command_name = data_source
|
||||
.active_commands()
|
||||
.find_map(|(_, command)| command.argument.is_none().then_some(command.name))
|
||||
.expect("expected at least one slash command without an argument");
|
||||
|
||||
let optional_argument_command_name = data_source
|
||||
.active_commands()
|
||||
.find_map(|(_, command)| {
|
||||
(command.argument.is_some()
|
||||
&& data_source.parse_slash_command(command.name).is_some())
|
||||
.then_some(command.name)
|
||||
})
|
||||
.expect("expected at least one slash command that accepts optional arguments");
|
||||
|
||||
let with_extra_text = format!("{no_argument_command_name} trailing");
|
||||
assert!(
|
||||
data_source.parse_slash_command(&with_extra_text).is_none(),
|
||||
"commands without arguments should reject non-whitespace suffixes"
|
||||
);
|
||||
|
||||
let with_whitespace = format!("{no_argument_command_name} ");
|
||||
let detected_with_whitespace = data_source
|
||||
.parse_slash_command(&with_whitespace)
|
||||
.expect("command with whitespace-only suffix should still be parsed");
|
||||
assert_eq!(
|
||||
detected_with_whitespace.command.name,
|
||||
no_argument_command_name
|
||||
);
|
||||
|
||||
let detected_without_argument = data_source
|
||||
.parse_slash_command(optional_argument_command_name)
|
||||
.expect("optional-argument command should parse without an argument");
|
||||
assert_eq!(detected_without_argument.argument, None);
|
||||
|
||||
let with_argument = format!("{optional_argument_command_name} prompt");
|
||||
let detected_with_argument = data_source
|
||||
.parse_slash_command(&with_argument)
|
||||
.expect("optional-argument command should parse with an argument");
|
||||
assert_eq!(detected_with_argument.argument.as_deref(), Some("prompt"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rename_tab_slash_command_arguments() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
let slash_command_data_source =
|
||||
input.read(&app, |input, _| input.slash_command_data_source.clone());
|
||||
|
||||
slash_command_data_source.read(&app, |data_source, _| {
|
||||
assert!(
|
||||
data_source
|
||||
.parse_slash_command(commands::RENAME_TAB.name)
|
||||
.is_none(),
|
||||
"expected /rename-tab without an argument not to parse"
|
||||
);
|
||||
|
||||
let detected_with_argument = data_source
|
||||
.parse_slash_command("/rename-tab Backend")
|
||||
.expect("expected /rename-tab to parse with an argument");
|
||||
assert_eq!(detected_with_argument.argument.as_deref(), Some("Backend"));
|
||||
|
||||
let detected_with_multi_word_argument = data_source
|
||||
.parse_slash_command("/rename-tab Backend API")
|
||||
.expect("expected /rename-tab to preserve the rest of the line as one argument");
|
||||
assert_eq!(
|
||||
detected_with_multi_word_argument.argument.as_deref(),
|
||||
Some("Backend API")
|
||||
);
|
||||
|
||||
let detected_with_empty_argument = data_source
|
||||
.parse_slash_command("/rename-tab ")
|
||||
.expect("expected /rename-tab to parse once the required argument is started");
|
||||
assert_eq!(detected_with_empty_argument.argument.as_deref(), Some(""));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_ai_commands_remain_active_when_ai_is_disabled() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
let slash_command_data_source =
|
||||
input.read(&app, |input, _| input.slash_command_data_source.clone());
|
||||
|
||||
// Disable AI globally.
|
||||
AISettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
report_if_error!(settings.is_any_ai_enabled.set_value(false, ctx));
|
||||
});
|
||||
|
||||
slash_command_data_source.read(&app, |data_source, _| {
|
||||
let active_command_names: Vec<&str> = data_source
|
||||
.active_commands()
|
||||
.map(|(_, command)| command.name)
|
||||
.collect();
|
||||
|
||||
// Commands that don't require AI should still be active.
|
||||
// `/rename-tab` is a good canary because it has no session-context requirements
|
||||
// other than ALWAYS.
|
||||
assert!(
|
||||
active_command_names.contains(&commands::RENAME_TAB.name),
|
||||
"/rename-tab should remain active when AI is off, got: {active_command_names:?}"
|
||||
);
|
||||
|
||||
// Commands that require AI should be filtered out.
|
||||
assert!(
|
||||
!active_command_names.contains(&commands::AGENT.name),
|
||||
"/agent should NOT be active when AI is off, got: {active_command_names:?}"
|
||||
);
|
||||
assert!(
|
||||
!active_command_names.contains(&commands::PLAN.name),
|
||||
"/plan should NOT be active when AI is off, got: {active_command_names:?}"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_until_empty_buffer_ignores_non_slash_edits() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
input.user_insert("echo hello", ctx);
|
||||
});
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.slash_command_model.update(ctx, |model, ctx| {
|
||||
model.disable(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
assert!(matches!(
|
||||
input.slash_command_model.as_ref(ctx).state(),
|
||||
SlashCommandEntryState::DisabledUntilEmptyBuffer
|
||||
));
|
||||
});
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.user_insert(" world", ctx);
|
||||
});
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
assert!(matches!(
|
||||
input.slash_command_model.as_ref(ctx).state(),
|
||||
SlashCommandEntryState::DisabledUntilEmptyBuffer
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_until_empty_buffer_reevaluates_when_slash_is_added_to_start() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
let slash_command_data_source =
|
||||
input.read(&app, |input, _| input.slash_command_data_source.clone());
|
||||
let detected_command_name = slash_command_data_source.read(&app, |data_source, _| {
|
||||
data_source
|
||||
.active_commands()
|
||||
.find_map(|(_, command)| {
|
||||
data_source
|
||||
.parse_slash_command(command.name)
|
||||
.is_some()
|
||||
.then(|| command.name.to_owned())
|
||||
})
|
||||
.expect("expected at least one active slash command")
|
||||
});
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
input.user_insert("echo hello", ctx);
|
||||
});
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.slash_command_model.update(ctx, |model, ctx| {
|
||||
model.disable(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.user_replace_editor_text(&detected_command_name, ctx);
|
||||
});
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
assert!(
|
||||
matches!(
|
||||
input.slash_command_model.as_ref(ctx).state(),
|
||||
SlashCommandEntryState::SlashCommand(_)
|
||||
),
|
||||
"adding '/' at the start of the buffer should re-enable slash command parsing"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_second_slash_in_command_token_sets_state_to_none() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(
|
||||
&mut app, None, /* history_file_commands */
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
input.user_insert("/foo/bar", ctx);
|
||||
});
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
assert!(matches!(
|
||||
input.slash_command_model.as_ref(ctx).state(),
|
||||
SlashCommandEntryState::None
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_returns_none_for_plain_text() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
let result = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command("fix the tests", ctx);
|
||||
assert!(
|
||||
matches!(result, SlashCommandEntryState::None),
|
||||
"plain text should not be detected as a command"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_finds_known_slash_command() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
let slash_command_data_source = &input.slash_command_data_source;
|
||||
let command_name = slash_command_data_source
|
||||
.as_ref(ctx)
|
||||
.active_commands()
|
||||
.find_map(|(_, command)| {
|
||||
slash_command_data_source
|
||||
.as_ref(ctx)
|
||||
.parse_slash_command(command.name)
|
||||
.is_some()
|
||||
.then(|| command.name.to_owned())
|
||||
})
|
||||
.expect("expected at least one active slash command");
|
||||
|
||||
let result = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command(&command_name, ctx);
|
||||
assert!(
|
||||
matches!(result, SlashCommandEntryState::SlashCommand(_)),
|
||||
"known command should be detected: {command_name}"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_returns_none_for_unknown_slash() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
let result = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command("/nonexistent-command-xyz", ctx);
|
||||
assert!(
|
||||
matches!(result, SlashCommandEntryState::None),
|
||||
"unknown /command should return None"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_matches_buffer_driven_detection() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
let command_name = input.read(&app, |input, ctx| {
|
||||
let ds = &input.slash_command_data_source;
|
||||
ds.as_ref(ctx)
|
||||
.active_commands()
|
||||
.find_map(|(_, command)| {
|
||||
ds.as_ref(ctx)
|
||||
.parse_slash_command(command.name)
|
||||
.is_some()
|
||||
.then(|| command.name.to_owned())
|
||||
})
|
||||
.expect("expected at least one active slash command")
|
||||
});
|
||||
|
||||
// Type the command into the buffer so the buffer-driven model processes it.
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
input.user_insert(&command_name, ctx);
|
||||
});
|
||||
|
||||
// Compare the buffer-driven state with the stateless detect_command result.
|
||||
input.read(&app, |input, ctx| {
|
||||
let buffer_state = input.slash_command_model.as_ref(ctx).state().clone();
|
||||
let detect_state = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command(&command_name, ctx);
|
||||
|
||||
let buffer_is_slash = matches!(buffer_state, SlashCommandEntryState::SlashCommand(_));
|
||||
let detect_is_slash = matches!(detect_state, SlashCommandEntryState::SlashCommand(_));
|
||||
assert_eq!(
|
||||
buffer_is_slash, detect_is_slash,
|
||||
"detect_command and buffer-driven detection should agree for '{command_name}'"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_parses_queue_with_argument() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
let result = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command("/queue fix the tests", ctx);
|
||||
|
||||
if let SlashCommandEntryState::SlashCommand(detected) = result {
|
||||
assert_eq!(detected.command.name, "/queue");
|
||||
assert_eq!(detected.argument.as_deref(), Some("fix the tests"));
|
||||
} else {
|
||||
// /queue may not be registered if the feature flag is off in tests.
|
||||
// That's fine — this test validates argument extraction when it is.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_command_returns_queue_with_no_argument() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.read(&app, |input, ctx| {
|
||||
let result = input
|
||||
.slash_command_model
|
||||
.as_ref(ctx)
|
||||
.detect_command("/queue", ctx);
|
||||
|
||||
// /queue requires an argument, so bare "/queue" should not be detected.
|
||||
// (StaticCommand with required argument rejects input without a space-delimited arg.)
|
||||
assert!(
|
||||
!matches!(result, SlashCommandEntryState::SlashCommand(_)),
|
||||
"/queue with no argument should not be detected as a complete slash command"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_queued_prompt_routes_plain_text_to_conversation() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
// Set up AI input mode so the input can interact with the AI controller.
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
});
|
||||
|
||||
// submit_queued_prompt with plain text should not panic or crash.
|
||||
// It routes through detect_command (returning None) and falls through
|
||||
// to send_user_query_in_new_conversation.
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.submit_queued_prompt("fix the tests".to_string(), ctx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_submit_queued_prompt_detects_slash_command() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(&mut app);
|
||||
|
||||
let terminal = add_window_with_bootstrapped_terminal(&mut app, None, None).await;
|
||||
let input = terminal.read(&app, |terminal, _| terminal.input().clone());
|
||||
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.set_input_mode_natural_language_detection(ctx);
|
||||
});
|
||||
|
||||
// Verify that submit_queued_prompt correctly detects slash commands.
|
||||
// Find a command that exists and has an optional argument.
|
||||
let command_with_arg = input.read(&app, |input, ctx| {
|
||||
let ds = &input.slash_command_data_source;
|
||||
ds.as_ref(ctx).active_commands().find_map(|(_, command)| {
|
||||
(command.argument.as_ref().is_some_and(|a| a.is_optional)
|
||||
&& ds.as_ref(ctx).parse_slash_command(command.name).is_some())
|
||||
.then(|| command.name.to_owned())
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(command_text) = command_with_arg {
|
||||
// submit_queued_prompt should detect the slash command and route through
|
||||
// execute_slash_command. This should not panic.
|
||||
input.update(&mut app, |input, ctx| {
|
||||
input.submit_queued_prompt(command_text, ctx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user