Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions crates/tui/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,127 @@ mod tests {
}
}

#[test]
fn command_registry_metadata_is_complete_and_palette_safe() {
for command in COMMANDS {
assert!(!command.name.is_empty(), "command name must not be empty");
assert_eq!(
command.name.trim(),
command.name,
"/{} command name must not need trimming",
command.name
);
assert!(
command
.name
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()),
"/{} command names must stay lowercase ASCII",
command.name
);

let expected_usage_prefix = format!("/{}", command.name);
assert!(
command.usage.starts_with(&expected_usage_prefix),
"/{} usage must start with its canonical slash command, got {:?}",
command.name,
command.usage
);

let description = command.description_for(Locale::En);
assert!(
!description.trim().is_empty(),
"/{} must have non-empty English help text",
command.name
);

let palette_command = command.palette_command();
assert!(
palette_command.starts_with(&expected_usage_prefix),
"/{} palette command must use the canonical command, got {:?}",
command.name,
palette_command
);
assert_eq!(
palette_command.ends_with(' '),
command.requires_argument(),
"/{} palette command spacing must match argument requirement",
command.name
);

for &alias in command.aliases {
assert!(
!alias.trim().is_empty(),
"/{} alias must not be empty",
command.name
);
assert_eq!(
alias.trim(),
alias,
"/{} alias /{alias} must not need trimming",
command.name
);
assert!(
!alias.starts_with('/'),
"/{} alias /{alias} must be stored without a slash",
command.name
);
assert!(
!alias.chars().any(char::is_whitespace),
"/{} alias /{alias} must not contain whitespace",
command.name
);
}
}
}

#[test]
fn command_info_resolves_canonical_names_and_aliases() {
for command in COMMANDS {
for lookup in [command.name.to_string(), format!("/{}", command.name)] {
let resolved = get_command_info(&lookup)
.unwrap_or_else(|| panic!("{lookup:?} should resolve to /{}", command.name));
assert_eq!(resolved.name, command.name);
}

for &alias in command.aliases {
for lookup in [alias.to_string(), format!("/{alias}")] {
let resolved = get_command_info(&lookup).unwrap_or_else(|| {
panic!("{lookup:?} should resolve to /{}", command.name)
});
assert_eq!(resolved.name, command.name);
}
}
}
}

#[test]
fn every_registered_command_has_a_help_topic() {
let mut app = create_test_app();
for command in COMMANDS {
let result = execute(&format!("/help {}", command.name), &mut app);
assert!(
!result.is_error,
"/help {} returned an error: {result:?}",
command.name
);
let message = result
.message
.unwrap_or_else(|| panic!("/help {} should return text", command.name));
assert!(
message.contains(command.name),
"/help {} should mention the command name, got {message:?}",
command.name
);
assert!(
message.contains(command.usage),
"/help {} should include usage {:?}, got {message:?}",
command.name,
command.usage
);
}
}

#[test]
fn context_command_opens_inspector_and_keeps_ctx_alias() {
let context = COMMANDS
Expand Down Expand Up @@ -1534,6 +1655,86 @@ mod tests {
name == "restore"
}

#[test]
fn slash_parser_preserves_arguments_after_the_command_name() {
let mut app = create_test_app();
let result = execute("/agent 2 review this carefully", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /agent to send a model instruction");
};
assert!(message.contains(r#"prompt: "review this carefully""#));
assert!(message.contains("max_depth: 2"));

let mut app = create_test_app();
let result = execute(" /relay ship command harness ", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /relay to send a model instruction");
};
assert!(message.contains("Requested relay focus: ship command harness"));

let mut app = create_test_app();
let result = execute("/rlm 3 inspect this corpus", &mut app);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected /rlm to send a model instruction");
};
assert!(message.contains(r#"content: "inspect this corpus""#));
assert!(message.contains("sub_rlm_max_depth: 3"));
}

#[test]
fn representative_command_groups_keep_dispatch_surfaces() {
let mut app = create_test_app();
let help = execute("/help clear", &mut app)
.message
.expect("/help clear should return text");
assert!(help.contains("clear"));
assert!(help.contains("/clear"));

let mut app = create_test_app();
let result = execute("/config", &mut app);
assert!(matches!(result.action, Some(AppAction::OpenConfigView)));

let mut app = create_test_app();
let result = execute("/relay command boundary", &mut app);
assert!(!result.is_error);
assert!(matches!(
result.action,
Some(AppAction::SendMessage(message))
if message.contains("Requested relay focus: command boundary")
));

let mut app = create_test_app();
let note_help = execute("/note help", &mut app)
.message
.expect("/note help should return text");
assert!(note_help.contains("Usage: /note"));

let mut app = create_test_app();
let result = execute("/hunt ship layer 2 | budget: 100", &mut app);
assert!(!result.is_error);
assert_eq!(app.hunt.quarry.as_deref(), Some("ship layer 2"));
assert_eq!(app.hunt.token_budget, Some(100));

let (mut app, _tmpdir, _guard) = create_isolated_test_app();
let skills = execute("/skills", &mut app)
.message
.expect("/skills should return text");
assert!(skills.contains("Skills location:"));

let mut app = create_test_app();
let result = execute("/task list", &mut app);
assert!(matches!(result.action, Some(AppAction::TaskList)));

let mut app = create_test_app();
let tokens = execute("/tokens", &mut app)
.message
.expect("/tokens should return text");
assert!(tokens.contains("deepseek-v4-pro"));
}

/// Smoke test: every entry in `COMMANDS` must dispatch to a real handler.
/// A dispatch miss surfaces as the fall-through `Unknown command:` error
/// message in `execute`. This catches the case where a new command is
Expand Down
52 changes: 52 additions & 0 deletions crates/tui/src/tui/command_palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,58 @@ mod tests {
assert!(!command_labels.contains(&"/deepseek"));
}

#[test]
fn command_palette_has_one_entry_for_every_registered_command() {
let tmp = TempDir::new().expect("tempdir");
let skills_dir = tmp.path().join("skills");
let mcp_config_path = tmp.path().join("mcp.json");
let entries = build_entries(
Locale::En,
skills_dir.as_path(),
tmp.path(),
mcp_config_path.as_path(),
None,
);

let command_entries = entries
.iter()
.filter(|entry| entry.section == PaletteSection::Command)
.collect::<Vec<_>>();
assert_eq!(command_entries.len(), commands::COMMANDS.len());

for command in commands::COMMANDS {
let label = format!("/{}", command.name);
let matching = command_entries
.iter()
.filter(|entry| entry.label == label)
.collect::<Vec<_>>();
assert_eq!(
matching.len(),
1,
"expected one palette entry for /{}",
command.name
);

let entry = matching[0];
assert_eq!(entry.command, command.palette_command());
assert!(
entry
.description
.contains(command.description_for(Locale::En)),
"/{} palette description should include command help text",
command.name
);
if command.requires_argument() {
assert!(
entry.description.contains(command.usage),
"/{} palette description should include usage {:?}",
command.name,
command.usage
);
}
}
}

#[test]
fn command_palette_inserts_model_command_for_argument_entry() {
let entries = build_entries(
Expand Down