| use std::sync::{Arc, Mutex}; |
|
|
| use clap::error::ErrorKind; |
| use clap::{Parser, Subcommand}; |
| use forge_api::{AgentInfo, Model, Template}; |
| use forge_domain::UserCommand; |
| use strum::{EnumProperty, IntoEnumIterator}; |
| use strum_macros::{EnumIter, EnumProperty}; |
|
|
| use crate::info::Info; |
|
|
| |
| |
| |
| |
| #[derive(Debug, Parser)] |
| #[command( |
| name = "forge_cmd", |
| no_binary_name = true, |
| disable_help_subcommand = true |
| )] |
| struct ClapCmd { |
| #[command(subcommand)] |
| sub: AppCommand, |
| } |
|
|
| |
| #[derive(Debug, Clone)] |
| pub struct AgentCommandRegistrationResult { |
| pub registered_count: usize, |
| pub skipped_conflicts: Vec<String>, |
| } |
|
|
| fn humanize_context_length(length: u64) -> String { |
| if length >= 1_000_000 { |
| format!("{:.1}M context", length as f64 / 1_000_000.0) |
| } else if length >= 1_000 { |
| format!("{:.1}K context", length as f64 / 1_000.0) |
| } else { |
| format!("{length} context") |
| } |
| } |
|
|
| impl From<&[Model]> for Info { |
| fn from(models: &[Model]) -> Self { |
| let mut info = Info::new(); |
|
|
| for model in models.iter() { |
| if let Some(context_length) = model.context_length { |
| info = info.add_key_value(&model.id, humanize_context_length(context_length)); |
| } else { |
| info = info.add_value(model.id.as_str()); |
| } |
| } |
|
|
| info |
| } |
| } |
|
|
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct ForgeCommand { |
| pub name: String, |
| pub description: String, |
| pub value: Option<String>, |
| } |
|
|
| #[derive(Debug)] |
| pub struct ForgeCommandManager { |
| commands: Arc<Mutex<Vec<ForgeCommand>>>, |
| } |
|
|
| impl Default for ForgeCommandManager { |
| fn default() -> Self { |
| let commands = Self::default_commands(); |
| ForgeCommandManager { commands: Arc::new(Mutex::new(commands)) } |
| } |
| } |
|
|
| impl ForgeCommandManager { |
| |
| |
| fn sanitize_agent_id(agent_id: &str) -> String { |
| agent_id |
| .to_lowercase() |
| .chars() |
| .map(|c| if c.is_alphanumeric() { c } else { '-' }) |
| .collect::<String>() |
| .split('-') |
| .filter(|s| !s.is_empty()) |
| .collect::<Vec<&str>>() |
| .join("-") |
| } |
|
|
| |
| fn is_reserved_command(name: &str) -> bool { |
| matches!( |
| name, |
| "agent" |
| | "forge" |
| | "muse" |
| | "sage" |
| | "help" |
| | "compact" |
| | "new" |
| | "info" |
| | "usage" |
| | "exit" |
| | "update" |
| | "dump" |
| | "model" |
| | "tools" |
| | "provider" |
| | "login" |
| | "logout" |
| | "retry" |
| | "conversations" |
| | "conversation-tree" |
| | "ct" |
| | "list" |
| | "commit" |
| | "rename" |
| | "rn" |
| | "config" |
| | "env" |
| | "config-model" |
| | "cm" |
| | "config-reload" |
| | "cr" |
| | "model-reset" |
| | "mr" |
| | "reasoning-effort" |
| | "re" |
| | "config-reasoning-effort" |
| | "cre" |
| | "config-commit-model" |
| | "ccm" |
| | "config-suggest-model" |
| | "csm" |
| | "config-edit" |
| | "ce" |
| | "skill" |
| | "edit" |
| | "ed" |
| | "commit-preview" |
| | "suggest" |
| | "s" |
| | "clone" |
| | "conversation-rename" |
| | "copy" |
| | "workspace-sync" |
| | "sync" |
| | "workspace-status" |
| | "sync-status" |
| | "workspace-info" |
| | "sync-info" |
| | "workspace-init" |
| | "sync-init" |
| ) |
| } |
|
|
| fn default_commands() -> Vec<ForgeCommand> { |
| AppCommand::iter() |
| .filter(|command| !command.is_internal()) |
| .map(|command| ForgeCommand { |
| name: command.name().to_string(), |
| description: command.usage().to_string(), |
| value: None, |
| }) |
| .collect::<Vec<_>>() |
| } |
|
|
| |
| pub fn register_all(&self, commands: Vec<forge_domain::Command>) { |
| let mut guard = self.commands.lock().unwrap(); |
|
|
| |
| guard.retain(|cmd| !cmd.description.starts_with("⚙ ")); |
|
|
| |
| let new_commands = commands.into_iter().map(|cmd| { |
| let name = cmd.name.clone(); |
| let description = format!("⚙ {}", cmd.description); |
| let value = cmd.prompt.clone(); |
|
|
| ForgeCommand { name, description, value } |
| }); |
|
|
| guard.extend(new_commands); |
|
|
| |
| guard.sort_by(|a, b| a.name.cmp(&b.name)); |
| } |
|
|
| |
| |
| pub fn register_agent_commands( |
| &self, |
| agents: Vec<AgentInfo>, |
| ) -> AgentCommandRegistrationResult { |
| let mut guard = self.commands.lock().unwrap(); |
| let mut result = |
| AgentCommandRegistrationResult { registered_count: 0, skipped_conflicts: Vec::new() }; |
|
|
| |
| guard.retain(|cmd| !cmd.name.starts_with("agent-")); |
|
|
| |
| for agent in agents { |
| let agent_id_str = agent.id.as_str(); |
| let sanitized_id = Self::sanitize_agent_id(agent_id_str); |
| let command_name = format!("agent-{sanitized_id}"); |
|
|
| |
| if Self::is_reserved_command(&command_name) { |
| result.skipped_conflicts.push(command_name); |
| continue; |
| } |
|
|
| let default_title = agent_id_str.to_string(); |
| let title = agent.title.as_ref().unwrap_or(&default_title); |
| let description = format!("🤖 Switch to {title} agent"); |
|
|
| guard.push(ForgeCommand { |
| name: command_name, |
| description, |
| value: Some(agent_id_str.to_string()), |
| }); |
|
|
| result.registered_count += 1; |
| } |
|
|
| |
| guard.sort_by(|a, b| a.name.cmp(&b.name)); |
|
|
| result |
| } |
|
|
| |
| fn find(&self, command: &str) -> Option<ForgeCommand> { |
| self.commands |
| .lock() |
| .unwrap() |
| .iter() |
| .find(|c| c.name == command) |
| .cloned() |
| } |
|
|
| |
| pub fn list(&self) -> Vec<ForgeCommand> { |
| self.commands.lock().unwrap().clone() |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| fn extract_command_value(&self, command: &ForgeCommand, parts: &[&str]) -> Option<String> { |
| |
|
|
| |
| let value_provided = if !parts.is_empty() { |
| Some(parts.join(" ")) |
| } else { |
| None |
| }; |
|
|
| |
| let value_default = self |
| .commands |
| .lock() |
| .unwrap() |
| .iter() |
| .find(|c| c.name == command.name) |
| .and_then(|cmd| cmd.value.clone()); |
|
|
| |
| match value_provided { |
| Some(value) if !value.trim().is_empty() => Some(value), |
| _ => value_default, |
| } |
| } |
|
|
| pub fn parse(&self, input: &str) -> anyhow::Result<AppCommand> { |
| |
| if input.trim().starts_with('!') { |
| return Ok(AppCommand::Shell( |
| input |
| .strip_prefix('!') |
| .unwrap_or_default() |
| .trim() |
| .to_string(), |
| )); |
| } |
|
|
| let trimmed = input.trim(); |
| let mut tokens = trimmed.split_ascii_whitespace(); |
| let first = tokens.next().unwrap_or(""); |
|
|
| |
| let is_command = first.starts_with('/') || first.starts_with(':'); |
| if !is_command { |
| return Ok(AppCommand::Message(input.to_string())); |
| } |
|
|
| |
| let bare = first |
| .strip_prefix('/') |
| .or_else(|| first.strip_prefix(':')) |
| .unwrap_or(first); |
| let command_prefix = first |
| .chars() |
| .next() |
| .filter(|c| *c == '/' || *c == ':') |
| .unwrap_or(':'); |
| let rest: Vec<&str> = tokens.collect(); |
|
|
| |
| let argv: Vec<&str> = std::iter::once(bare).chain(rest.iter().copied()).collect(); |
| let parameters: Vec<String> = rest.iter().map(|s| s.to_string()).collect(); |
|
|
| match ClapCmd::try_parse_from(&argv) { |
| Ok(mut cmd) => { |
| |
| match &mut cmd.sub { |
| AppCommand::Commit { args, max_diff_size } => { |
| *max_diff_size = args.iter().find_map(|p| p.parse::<usize>().ok()); |
| } |
| AppCommand::Rename { name } => { |
| let n = name.join(" "); |
| let n = n.trim().to_string(); |
| if n.is_empty() { |
| return Err(anyhow::anyhow!( |
| "Usage: :rename <name>. Please provide a name for the conversation." |
| )); |
| } |
| } |
| _ => {} |
| } |
| Ok(cmd.sub) |
| } |
| Err(clap_err) => { |
| |
| |
| let command_name = bare; |
|
|
| |
| if (command_name == "rename" || command_name == "rn") && rest.is_empty() { |
| return Err(anyhow::anyhow!( |
| "Usage: :rename <name>. Please provide a name for the conversation." |
| )); |
| } |
|
|
| |
| if command_name.starts_with("agent-") { |
| if let Some(found_command) = self.find(command_name) |
| && let Some(agent_id) = &found_command.value |
| { |
| return Ok(AppCommand::AgentSwitch(agent_id.clone())); |
| } |
| return Err(anyhow::anyhow!( |
| "/{command_name} is not a valid agent command" |
| )); |
| } |
|
|
| |
| if let Some(command) = self.find(command_name) { |
| let rest_parts: Vec<&str> = rest.to_vec(); |
| let template = Template::new( |
| self.extract_command_value(&command, &rest_parts) |
| .unwrap_or_default(), |
| ); |
| return Ok(AppCommand::Custom(UserCommand::new( |
| command.name.clone(), |
| template, |
| parameters, |
| ))); |
| } |
|
|
| |
| if clap_err.kind() == ErrorKind::InvalidSubcommand { |
| return Err(anyhow::anyhow!( |
| "Unknown command '{command_prefix}{command_name}'. Run '{command_prefix}help' to list available commands." |
| )); |
| } |
|
|
| |
| let rendered = clap_err.render().to_string(); |
| let cleaned = rendered.replace("forge_cmd", "forge"); |
| Err(anyhow::anyhow!("{}", cleaned.trim())) |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| #[derive(Debug, Clone, PartialEq, Eq, EnumProperty, EnumIter, Subcommand)] |
| pub enum AppCommand { |
| |
| |
| #[strum(props(usage = "Display effective resolved configuration"))] |
| #[command(aliases = ["env", "e"])] |
| Config, |
|
|
| |
| |
| #[strum(props(usage = "Set the global model [alias: cm]"))] |
| #[command(name = "config-model", alias = "cm")] |
| ConfigModel, |
|
|
| |
| |
| |
| #[strum(props(usage = "Reset session overrides to global config [alias: cr]"))] |
| #[command(name = "config-reload", aliases = ["cr", "model-reset", "mr"])] |
| ConfigReload, |
|
|
| |
| |
| #[strum(props(usage = "Set reasoning effort for current session [alias: re]"))] |
| #[command(name = "reasoning-effort", alias = "re")] |
| ReasoningEffort, |
|
|
| |
| |
| |
| #[strum(props(usage = "Set reasoning effort in global config [alias: cre]"))] |
| #[command(name = "config-reasoning-effort", alias = "cre")] |
| ConfigReasoningEffort, |
|
|
| |
| |
| |
| #[strum(props(usage = "Set the model used for commit message generation [alias: ccm]"))] |
| #[command(name = "config-commit-model", alias = "ccm")] |
| ConfigCommitModel, |
|
|
| |
| |
| |
| #[strum(props(usage = "Set the model used for suggest generation [alias: csm]"))] |
| #[command(name = "config-suggest-model", alias = "csm")] |
| ConfigSuggestModel, |
|
|
| |
| |
| #[strum(props(usage = "Open global config file in an editor [alias: ce]"))] |
| #[command(name = "config-edit", alias = "ce")] |
| ConfigEdit, |
|
|
| |
| |
| #[strum(props(usage = "List all available skills"))] |
| Skill, |
|
|
| |
| |
| #[strum(props(usage = "Open external editor to write a prompt [alias: ed]"))] |
| #[command(alias = "ed")] |
| Edit { |
| |
| #[arg(trailing_var_arg = true, num_args = 0..)] |
| content: Vec<String>, |
| }, |
|
|
| |
| |
| #[strum(props(usage = "Preview AI-generated commit message"))] |
| #[command(name = "commit-preview")] |
| CommitPreview, |
|
|
| |
| |
| #[strum(props(usage = "Generate shell command from natural language [alias: s]"))] |
| #[command(alias = "s")] |
| Suggest { |
| |
| #[arg(trailing_var_arg = true, num_args = 0.., allow_hyphen_values = true)] |
| description: Vec<String>, |
| }, |
|
|
| |
| |
| #[strum(props(usage = "Clone current or selected conversation"))] |
| Clone { |
| |
| |
| id: Option<String>, |
| }, |
|
|
| |
| |
| #[strum(props(usage = "Rename a conversation interactively"))] |
| #[command(name = "conversation-rename")] |
| ConversationRename { |
| |
| |
| #[arg(trailing_var_arg = true, num_args = 0..)] |
| name: Vec<String>, |
| }, |
|
|
| |
| |
| #[strum(props(usage = "Copy last AI response to clipboard"))] |
| Copy, |
|
|
| |
| |
| #[strum(props(usage = "Sync current workspace for semantic search [alias: sync]"))] |
| #[command(name = "workspace-sync", alias = "sync")] |
| WorkspaceSync, |
|
|
| |
| |
| #[strum(props(usage = "Show sync status of all workspace files"))] |
| #[command(name = "workspace-status", alias = "sync-status")] |
| WorkspaceStatus, |
|
|
| |
| |
| #[strum(props(usage = "Show workspace information with sync details"))] |
| #[command(name = "workspace-info", alias = "sync-info")] |
| WorkspaceInfo, |
|
|
| |
| |
| #[strum(props(usage = "Initialize a new workspace without syncing files"))] |
| #[command(name = "workspace-init", alias = "sync-init")] |
| WorkspaceInit, |
|
|
| |
| |
| #[strum(props(usage = "Compact the conversation context"))] |
| Compact, |
|
|
| |
| |
| #[strum(props(usage = "Start a new conversation"))] |
| New, |
|
|
| |
| |
| #[strum(props(usage = "Send a regular message"))] |
| #[command(skip)] |
| Message(String), |
|
|
| |
| |
| #[strum(props(usage = "Display system information"))] |
| Info, |
|
|
| |
| #[strum(props(usage = "Shows usage information (tokens & requests)"))] |
| Usage, |
|
|
| |
| #[strum(props(usage = "Exit the application"))] |
| Exit, |
|
|
| |
| #[strum(props(usage = "Updates to the latest compatible version of forge"))] |
| Update, |
|
|
| |
| |
| #[strum(props(usage = "Enable implementation mode with code changes"))] |
| #[command(name = "act", alias = "forge")] |
| Forge, |
|
|
| |
| |
| #[strum(props(usage = "Enable planning mode without code changes"))] |
| #[command(name = "plan", alias = "muse")] |
| Muse, |
|
|
| |
| |
| #[strum(props( |
| usage = "Enable research mode for systematic codebase exploration and analysis" |
| ))] |
| Sage, |
|
|
| |
| |
| #[strum(props(usage = "Enable help mode for tool questions"))] |
| #[command(name = "help")] |
| Help, |
|
|
| |
| #[strum(props(usage = "Save conversation as JSON or HTML (use /dump --html for HTML format)"))] |
| Dump { |
| |
| #[arg(long)] |
| html: bool, |
| }, |
|
|
| |
| |
| #[strum(props(usage = "Switch to a different model"))] |
| #[command(alias = "m")] |
| Model, |
|
|
| |
| |
| #[strum(props(usage = "List all available tools with their descriptions and schema"))] |
| #[command(alias = "t")] |
| Tools, |
|
|
| |
| #[command(skip)] |
| Custom(UserCommand), |
|
|
| |
| |
| #[strum(props(usage = "Execute a native shell command"))] |
| #[command(skip)] |
| Shell(String), |
|
|
| |
| #[strum(props(usage = "Switch to an agent interactively"))] |
| #[command(alias = "a")] |
| Agent, |
|
|
| |
| #[strum(props(usage = "Allows you to configure provider"))] |
| #[command(name = "provider", aliases = ["login", "provider-login"])] |
| Login, |
|
|
| |
| #[strum(props(usage = "Logout from configured provider"))] |
| Logout, |
|
|
| |
| #[strum(props(usage = "Retry the last command"))] |
| #[command(alias = "r")] |
| Retry, |
|
|
| |
| #[strum(props(usage = "List all conversations for the active workspace"))] |
| #[command(name = "conversation", aliases = ["conversations", "c"])] |
| Conversations { |
| |
| |
| id: Option<String>, |
| }, |
|
|
| |
| #[strum(props( |
| usage = "Show nested conversations spawned by the current conversation [alias: ct]" |
| ))] |
| #[command(name = "conversation-tree", alias = "ct")] |
| ConversationTree, |
|
|
| |
| #[strum(props(usage = "Delete a conversation permanently"))] |
| #[command(skip)] |
| Delete, |
|
|
| |
| #[strum(props(usage = "Rename the current conversation. Usage: :rename <name>"))] |
| #[command(alias = "rn")] |
| Rename { |
| |
| #[arg(trailing_var_arg = true, required = true)] |
| name: Vec<String>, |
| }, |
|
|
| |
| #[strum(props(usage = "Switch directly to a specific agent"))] |
| #[command(skip)] |
| AgentSwitch(String), |
|
|
| |
| |
| |
| |
| |
| #[strum(props( |
| usage = "Generate AI commit message and commit changes. Format: :commit <max-diff|preview>" |
| ))] |
| Commit { |
| |
| #[arg(trailing_var_arg = true, num_args = 0..)] |
| args: Vec<String>, |
| |
| #[clap(skip)] |
| max_diff_size: Option<usize>, |
| }, |
|
|
| |
| #[strum(props(usage = "Index the current workspace for semantic search"))] |
| Index, |
| } |
|
|
| impl AppCommand { |
| pub fn name(&self) -> &str { |
| match self { |
| AppCommand::Compact => "compact", |
| AppCommand::New => "new", |
| AppCommand::Message(_) => "message", |
| AppCommand::Update => "update", |
| AppCommand::Info => "info", |
| AppCommand::Usage => "usage", |
| AppCommand::Exit => "exit", |
| AppCommand::Forge => "forge", |
| AppCommand::Muse => "muse", |
| AppCommand::Sage => "sage", |
| AppCommand::Help => "help", |
| AppCommand::Commit { .. } => "commit", |
| AppCommand::Dump { .. } => "dump", |
| AppCommand::Model => "model", |
| AppCommand::Tools => "tools", |
| AppCommand::Custom(event) => &event.name, |
| AppCommand::Shell(_) => "!shell", |
| AppCommand::Agent => "agent", |
| AppCommand::Login => "login", |
| AppCommand::Logout => "logout", |
| AppCommand::Retry => "retry", |
| AppCommand::Conversations { .. } => "conversation", |
| AppCommand::ConversationTree => "conversation-tree", |
| AppCommand::Delete => "delete", |
| AppCommand::Rename { .. } => "rename", |
| AppCommand::AgentSwitch(agent_id) => agent_id, |
| AppCommand::Index => "index", |
| AppCommand::Config => "config", |
| AppCommand::ConfigModel => "config-model", |
| AppCommand::ConfigReload => "config-reload", |
| AppCommand::ReasoningEffort => "reasoning-effort", |
| AppCommand::ConfigReasoningEffort => "config-reasoning-effort", |
| AppCommand::ConfigCommitModel => "config-commit-model", |
| AppCommand::ConfigSuggestModel => "config-suggest-model", |
| AppCommand::ConfigEdit => "config-edit", |
| AppCommand::Skill => "skill", |
| AppCommand::Edit { .. } => "edit", |
| AppCommand::CommitPreview => "commit-preview", |
| AppCommand::Suggest { .. } => "suggest", |
| AppCommand::Clone { .. } => "clone", |
| AppCommand::ConversationRename { .. } => "conversation-rename", |
| AppCommand::Copy => "copy", |
| AppCommand::WorkspaceSync => "workspace-sync", |
| AppCommand::WorkspaceStatus => "workspace-status", |
| AppCommand::WorkspaceInfo => "workspace-info", |
| AppCommand::WorkspaceInit => "workspace-init", |
| } |
| } |
|
|
| |
| pub fn usage(&self) -> &str { |
| self.get_str("usage").unwrap() |
| } |
|
|
| |
| |
| pub fn is_internal(&self) -> bool { |
| matches!( |
| self, |
| AppCommand::Message(_) |
| | AppCommand::Custom(_) |
| | AppCommand::Shell(_) |
| | AppCommand::AgentSwitch(_) |
| | AppCommand::Rename { .. } |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| pub fn is_agent_switch(&self) -> bool { |
| matches!( |
| self, |
| AppCommand::Forge | AppCommand::Muse | AppCommand::Sage |
| ) |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use std::fmt::Display; |
|
|
| use colored::Colorize; |
| use console::strip_ansi_codes; |
| use forge_api::{ |
| AnyProvider, InputModality, Model, ModelId, ModelSource, ProviderId, ProviderResponse, |
| }; |
| use forge_domain::Provider; |
| use pretty_assertions::assert_eq; |
| use url::Url; |
|
|
| use super::*; |
| use crate::display_constants::markers; |
|
|
| |
| #[derive(Clone)] |
| struct CliModel(Model); |
|
|
| impl Display for CliModel { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| write!(f, "{}", self.0.id)?; |
|
|
| let mut info_parts = Vec::new(); |
|
|
| if let Some(limit) = self.0.context_length { |
| if limit >= 1_000_000 { |
| info_parts.push(format!("{}M", limit / 1_000_000)); |
| } else if limit >= 1000 { |
| info_parts.push(format!("{}k", limit / 1000)); |
| } else { |
| info_parts.push(format!("{limit}")); |
| } |
| } |
|
|
| if self.0.tools_supported == Some(true) { |
| info_parts.push("🛠️".to_string()); |
| } |
|
|
| if !info_parts.is_empty() { |
| let info = format!("[ {} ]", info_parts.join(" ")); |
| write!(f, " {}", info.dimmed())?; |
| } |
|
|
| Ok(()) |
| } |
| } |
|
|
| |
| #[derive(Clone)] |
| struct CliProvider(AnyProvider); |
|
|
| impl Display for CliProvider { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let name_width = ProviderId::built_in_providers() |
| .iter() |
| .map(|id| id.to_string().len()) |
| .max() |
| .unwrap_or(10); |
|
|
| let name = self.0.id().to_string(); |
|
|
| match &self.0 { |
| AnyProvider::Url(provider) => { |
| write!(f, "{} {:<width$}", "✓".green(), name, width = name_width)?; |
| if let Some(domain) = provider.url.domain() { |
| write!(f, " [{domain}]")?; |
| } else { |
| write!(f, " {}", markers::EMPTY)?; |
| } |
| } |
| AnyProvider::Template(_) => { |
| write!(f, " {name:<name_width$} {}", markers::EMPTY)?; |
| } |
| } |
| Ok(()) |
| } |
| } |
|
|
| #[test] |
| fn test_extract_command_value_with_provided_value() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts = vec!["arg1", "arg2"]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, Some(String::from("arg1 arg2"))); |
| } |
|
|
| #[test] |
| fn test_extract_command_value_with_empty_parts_default_value() { |
| |
| let cmd_manager = ForgeCommandManager { |
| commands: Arc::new(Mutex::new(vec![ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: Some(String::from("default_value")), |
| }])), |
| }; |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts: Vec<&str> = vec![]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, Some(String::from("default_value"))); |
| } |
|
|
| #[test] |
| fn test_extract_command_value_with_empty_string_parts() { |
| |
| let cmd_manager = ForgeCommandManager { |
| commands: Arc::new(Mutex::new(vec![ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: Some(String::from("default_value")), |
| }])), |
| }; |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts = vec![""]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, Some(String::from("default_value"))); |
| } |
|
|
| #[test] |
| fn test_extract_command_value_with_whitespace_parts() { |
| |
| let cmd_manager = ForgeCommandManager { |
| commands: Arc::new(Mutex::new(vec![ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: Some(String::from("default_value")), |
| }])), |
| }; |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts = vec![" "]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, Some(String::from("default_value"))); |
| } |
|
|
| #[test] |
| fn test_extract_command_value_no_default_no_provided() { |
| |
| let cmd_manager = ForgeCommandManager { |
| commands: Arc::new(Mutex::new(vec![ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }])), |
| }; |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts: Vec<&str> = vec![]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, None); |
| } |
|
|
| #[test] |
| fn test_extract_command_value_provided_overrides_default() { |
| |
| let cmd_manager = ForgeCommandManager { |
| commands: Arc::new(Mutex::new(vec![ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: Some(String::from("default_value")), |
| }])), |
| }; |
| let command = ForgeCommand { |
| name: String::from("/test"), |
| description: String::from("Test command"), |
| value: None, |
| }; |
| let parts = vec!["provided_value"]; |
|
|
| |
| let result = cmd_manager.extract_command_value(&command, &parts); |
|
|
| |
| assert_eq!(result, Some(String::from("provided_value"))); |
| } |
| #[test] |
| fn test_parse_shell_command() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse("!ls -la").unwrap(); |
|
|
| |
| match result { |
| AppCommand::Shell(cmd) => assert_eq!(cmd, "ls -la"), |
| _ => panic!("Expected Shell command, got {result:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_shell_command_empty() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse("!").unwrap(); |
|
|
| |
| match result { |
| AppCommand::Shell(cmd) => assert_eq!(cmd, ""), |
| _ => panic!("Expected Shell command, got {result:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_shell_command_with_whitespace() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse("! echo 'test' ").unwrap(); |
|
|
| |
| match result { |
| AppCommand::Shell(cmd) => assert_eq!(cmd, "echo 'test'"), |
| _ => panic!("Expected Shell command, got {result:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_shell_command_not_in_default_commands() { |
| |
| let manager = ForgeCommandManager::default(); |
| let commands = manager.list(); |
|
|
| |
| let contains_shell = commands.iter().any(|cmd| cmd.name == "!shell"); |
| assert!( |
| !contains_shell, |
| "Shell command should not be in default commands" |
| ); |
| } |
| #[test] |
| fn test_parse_list_command() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse("/conversation").unwrap(); |
|
|
| |
| match result { |
| AppCommand::Conversations { .. } => { |
| |
| } |
| _ => panic!("Expected List command, got {result:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_conversation_with_id() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let actual = cmd_manager |
| .parse("/conversation 550e8400-e29b-41d4-a716-446655440000") |
| .unwrap(); |
|
|
| |
| let expected = AppCommand::Conversations { |
| id: Some("550e8400-e29b-41d4-a716-446655440000".to_string()), |
| }; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_list_command_in_default_commands() { |
| |
| let manager = ForgeCommandManager::default(); |
| let commands = manager.list(); |
|
|
| |
| let contains_list = commands.iter().any(|cmd| cmd.name == "conversation"); |
| assert!( |
| contains_list, |
| "Conversations command should be in default commands" |
| ); |
| } |
|
|
| #[test] |
| fn test_sanitize_agent_id_basic() { |
| |
| let fixture = "test-agent"; |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); |
| let expected = "test-agent"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_sanitize_agent_id_with_spaces() { |
| |
| let fixture = "test agent name"; |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); |
| let expected = "test-agent-name"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_sanitize_agent_id_with_special_chars() { |
| |
| let fixture = "test@agent#name!"; |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); |
| let expected = "test-agent-name"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_sanitize_agent_id_uppercase() { |
| |
| let fixture = "TestAgent"; |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); |
| let expected = "testagent"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_is_reserved_command() { |
| |
| assert!(ForgeCommandManager::is_reserved_command("agent")); |
| assert!(ForgeCommandManager::is_reserved_command("forge")); |
| assert!(ForgeCommandManager::is_reserved_command("muse")); |
| assert!(!ForgeCommandManager::is_reserved_command("agent-custom")); |
| assert!(!ForgeCommandManager::is_reserved_command("custom")); |
| } |
|
|
| #[test] |
| fn test_register_agent_commands() { |
| |
| let fixture = ForgeCommandManager::default(); |
| let agents = vec![ |
| forge_domain::AgentInfo::default() |
| .id("test-agent") |
| .title("Test Agent".to_string()), |
| forge_domain::AgentInfo::default() |
| .id("another") |
| .title("Another Agent".to_string()), |
| ]; |
|
|
| |
| let result = fixture.register_agent_commands(agents); |
|
|
| |
| assert_eq!(result.registered_count, 2); |
| assert_eq!(result.skipped_conflicts.len(), 0); |
|
|
| |
| let commands = fixture.list(); |
| let agent_commands: Vec<_> = commands |
| .iter() |
| .filter(|cmd| cmd.name.starts_with("agent-")) |
| .collect(); |
|
|
| assert_eq!(agent_commands.len(), 2); |
| assert!( |
| agent_commands |
| .iter() |
| .any(|cmd| cmd.name == "agent-test-agent") |
| ); |
| assert!(agent_commands.iter().any(|cmd| cmd.name == "agent-another")); |
| } |
|
|
| #[test] |
| fn test_parse_agent_switch_command() { |
| |
| let fixture = ForgeCommandManager::default(); |
| let agents = vec![ |
| forge_domain::AgentInfo::default() |
| .id("test-agent") |
| .title("Test Agent".to_string()), |
| ]; |
| let _result = fixture.register_agent_commands(agents); |
|
|
| |
| let actual = fixture.parse("/agent-test-agent").unwrap(); |
|
|
| |
| match actual { |
| AppCommand::AgentSwitch(agent_id) => assert_eq!(agent_id, "test-agent"), |
| _ => panic!("Expected AgentSwitch command, got {actual:?}"), |
| } |
| } |
|
|
| fn create_model_fixture( |
| id: &str, |
| context_length: Option<u64>, |
| tools_supported: Option<bool>, |
| ) -> Model { |
| Model { |
| id: ModelId::new(id), |
| name: None, |
| description: None, |
| context_length, |
| tools_supported, |
| supports_parallel_tool_calls: None, |
| supports_reasoning: None, |
| input_modalities: vec![InputModality::Text], |
| } |
| } |
|
|
| #[test] |
| fn test_cli_model_display_with_context_and_tools() { |
| let fixture = create_model_fixture("gpt-4", Some(128000), Some(true)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "gpt-4 [ 128k 🛠️ ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_with_large_context() { |
| let fixture = create_model_fixture("claude-3", Some(2000000), Some(true)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "claude-3 [ 2M 🛠️ ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_with_small_context() { |
| let fixture = create_model_fixture("small-model", Some(512), Some(false)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "small-model [ 512 ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_with_context_only() { |
| let fixture = create_model_fixture("text-model", Some(4096), Some(false)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "text-model [ 4k ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_with_tools_only() { |
| let fixture = create_model_fixture("tool-model", None, Some(true)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "tool-model [ 🛠️ ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_empty_context_and_no_tools() { |
| let fixture = create_model_fixture("basic-model", None, Some(false)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "basic-model"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_empty_context_and_none_tools() { |
| let fixture = create_model_fixture("unknown-model", None, None); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "unknown-model"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_exact_thousands() { |
| let fixture = create_model_fixture("exact-k", Some(8000), Some(true)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "exact-k [ 8k 🛠️ ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_exact_millions() { |
| let fixture = create_model_fixture("exact-m", Some(1000000), Some(true)); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "exact-m [ 1M 🛠️ ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_edge_case_999() { |
| let fixture = create_model_fixture("edge-999", Some(999), None); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "edge-999 [ 999 ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_model_display_edge_case_1001() { |
| let fixture = create_model_fixture("edge-1001", Some(1001), None); |
| let formatted = format!("{}", CliModel(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "edge-1001 [ 1k ]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_provider_display_minimal() { |
| let fixture = AnyProvider::Url(Provider { |
| id: ProviderId::OPENAI, |
| provider_type: forge_domain::ProviderType::Llm, |
| response: Some(ProviderResponse::OpenAI), |
| url: Url::parse("https://api.openai.com/v1/chat/completions").unwrap(), |
| auth_methods: vec![forge_domain::AuthMethod::ApiKey], |
| url_params: vec![], |
| credential: None, |
| custom_headers: None, |
| models: Some(ModelSource::Url( |
| Url::parse("https://api.openai.com/v1/models").unwrap(), |
| )), |
| }); |
| let formatted = format!("{}", CliProvider(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "✓ OpenAI [api.openai.com]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_provider_display_with_subdomain() { |
| let fixture = AnyProvider::Url(Provider { |
| id: ProviderId::OPEN_ROUTER, |
| provider_type: forge_domain::ProviderType::Llm, |
| response: Some(ProviderResponse::OpenAI), |
| url: Url::parse("https://openrouter.ai/api/v1/chat/completions").unwrap(), |
| auth_methods: vec![forge_domain::AuthMethod::ApiKey], |
| url_params: vec![], |
| credential: None, |
| custom_headers: None, |
| models: Some(ModelSource::Url( |
| Url::parse("https://openrouter.ai/api/v1/models").unwrap(), |
| )), |
| }); |
| let formatted = format!("{}", CliProvider(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "✓ OpenRouter [openrouter.ai]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_provider_display_no_domain() { |
| let fixture = AnyProvider::Url(Provider { |
| id: ProviderId::FORGE, |
| provider_type: forge_domain::ProviderType::Llm, |
| response: Some(ProviderResponse::OpenAI), |
| url: Url::parse("http://localhost:8080/chat/completions").unwrap(), |
| auth_methods: vec![forge_domain::AuthMethod::ApiKey], |
| url_params: vec![], |
| credential: None, |
| custom_headers: None, |
| models: Some(ModelSource::Url( |
| Url::parse("http://localhost:8080/models").unwrap(), |
| )), |
| }); |
| let formatted = format!("{}", CliProvider(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = "✓ Forge [localhost]"; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_provider_display_template() { |
| let fixture = AnyProvider::Template(Provider { |
| id: ProviderId::ANTHROPIC, |
| provider_type: Default::default(), |
| response: Some(ProviderResponse::Anthropic), |
| url: Template::new("https://api.anthropic.com/v1/messages"), |
| auth_methods: vec![forge_domain::AuthMethod::ApiKey], |
| url_params: vec![], |
| credential: None, |
| custom_headers: None, |
| models: Some(ModelSource::Url(Template::new( |
| "https://api.anthropic.com/v1/models", |
| ))), |
| }); |
| let formatted = format!("{}", CliProvider(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = format!(" Anthropic {}", markers::EMPTY); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_cli_provider_display_ip_address() { |
| let fixture = AnyProvider::Url(Provider { |
| id: ProviderId::FORGE, |
| provider_type: forge_domain::ProviderType::Llm, |
| response: Some(ProviderResponse::OpenAI), |
| url: Url::parse("http://192.168.1.1:8080/chat/completions").unwrap(), |
| auth_methods: vec![forge_domain::AuthMethod::ApiKey], |
| url_params: vec![], |
| credential: None, |
| custom_headers: None, |
| models: Some(ModelSource::Url( |
| Url::parse("http://192.168.1.1:8080/models").unwrap(), |
| )), |
| }); |
| let formatted = format!("{}", CliProvider(fixture)); |
| let actual = strip_ansi_codes(&formatted); |
| let expected = format!("✓ Forge {}", markers::EMPTY); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_parse_commit_command() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/commit").unwrap(); |
| match actual { |
| AppCommand::Commit { max_diff_size, .. } => { |
| assert_eq!(max_diff_size, None); |
| } |
| _ => panic!("Expected Commit command, got {actual:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_commit_command_with_preview() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/commit preview").unwrap(); |
| match actual { |
| AppCommand::Commit { max_diff_size, .. } => { |
| assert_eq!(max_diff_size, None); |
| } |
| _ => panic!("Expected Commit command with preview, got {actual:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_commit_command_with_max_diff() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/commit 5000").unwrap(); |
| match actual { |
| AppCommand::Commit { max_diff_size, .. } => { |
| assert_eq!(max_diff_size, Some(5000)); |
| } |
| _ => panic!("Expected Commit command with max_diff_size, got {actual:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_commit_command_with_all_flags() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/commit preview 10000").unwrap(); |
| match actual { |
| AppCommand::Commit { max_diff_size, .. } => { |
| assert_eq!(max_diff_size, Some(10000)); |
| } |
| _ => panic!("Expected Commit command with all flags, got {actual:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_commit_command_in_default_commands() { |
| let manager = ForgeCommandManager::default(); |
| let commands = manager.list(); |
| let contains_commit = commands.iter().any(|cmd| cmd.name == "commit"); |
| assert!( |
| contains_commit, |
| "Commit command should be in default commands" |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_invalid_agent_command() { |
| |
| let fixture = ForgeCommandManager::default(); |
|
|
| |
| let result = fixture.parse("/agent-nonexistent"); |
|
|
| |
| assert!(result.is_err()); |
| assert!( |
| result |
| .unwrap_err() |
| .to_string() |
| .contains("not a valid agent command") |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_invalid_command_with_colon_returns_helpful_error() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse(":celar").unwrap_err().to_string(); |
| let expected = |
| "Unknown command ':celar'. Run ':help' to list available commands.".to_string(); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_parse_invalid_command_with_slash_returns_helpful_error() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/celar").unwrap_err().to_string(); |
| let expected = |
| "Unknown command '/celar'. Run '/help' to list available commands.".to_string(); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_parse_tool_command() { |
| |
| let fixture = ForgeCommandManager::default(); |
|
|
| |
| let result = fixture.parse("/tools").unwrap(); |
|
|
| |
| match result { |
| AppCommand::Tools => { |
| |
| } |
| _ => panic!("Expected Tool command, got {result:?}"), |
| } |
| } |
|
|
| #[test] |
| fn test_parse_dump_command_json() { |
| |
| let fixture = ForgeCommandManager::default(); |
|
|
| |
| let actual = fixture.parse("/dump").unwrap(); |
|
|
| |
| let expected = AppCommand::Dump { html: false }; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_parse_dump_command_html_with_flag() { |
| |
| let fixture = ForgeCommandManager::default(); |
|
|
| |
| let actual = fixture.parse("/dump --html").unwrap(); |
|
|
| |
| let expected = AppCommand::Dump { html: true }; |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_parse_rename_command() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/rename my-session").unwrap(); |
| assert_eq!( |
| actual, |
| AppCommand::Rename { name: vec!["my-session".to_string()] } |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_rename_command_multi_word() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/rename auth refactor work").unwrap(); |
| assert_eq!( |
| actual, |
| AppCommand::Rename { |
| name: vec![ |
| "auth".to_string(), |
| "refactor".to_string(), |
| "work".to_string() |
| ] |
| } |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_rename_command_no_name() { |
| let fixture = ForgeCommandManager::default(); |
| let result = fixture.parse("/rename"); |
| assert!(result.is_err()); |
| assert!(result.unwrap_err().to_string().contains("provide a name")); |
| } |
|
|
| #[test] |
| fn test_parse_rename_alias() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/rn my-session").unwrap(); |
| assert_eq!( |
| actual, |
| AppCommand::Rename { name: vec!["my-session".to_string()] } |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_rename_trims_whitespace() { |
| let fixture = ForgeCommandManager::default(); |
| let actual = fixture.parse("/rename my title ").unwrap(); |
| assert_eq!( |
| actual, |
| AppCommand::Rename { name: vec!["my".to_string(), "title".to_string()] } |
| ); |
| } |
|
|
| #[test] |
| fn test_rename_is_reserved_command() { |
| assert!(ForgeCommandManager::is_reserved_command("rename")); |
| assert!(ForgeCommandManager::is_reserved_command("rn")); |
| } |
|
|
| #[test] |
| fn test_rename_command_name() { |
| let cmd = AppCommand::Rename { name: vec!["test".to_string()] }; |
| assert_eq!(cmd.name(), "rename"); |
| } |
|
|
| #[test] |
| fn test_parse_suggest_with_dash_prefixed_tokens() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse(":suggest --- date").unwrap(); |
|
|
| |
| assert_eq!( |
| result, |
| AppCommand::Suggest { description: vec!["---".to_string(), "date".to_string()] } |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_suggest_with_double_dash_flags() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse(":suggest --date tomorrow").unwrap(); |
|
|
| |
| assert_eq!( |
| result, |
| AppCommand::Suggest { |
| description: vec!["--date".to_string(), "tomorrow".to_string()] |
| } |
| ); |
| } |
|
|
| #[test] |
| fn test_parse_suggest_with_single_dash() { |
| |
| let cmd_manager = ForgeCommandManager::default(); |
|
|
| |
| let result = cmd_manager.parse(":suggest -v file.txt").unwrap(); |
|
|
| |
| assert_eq!( |
| result, |
| AppCommand::Suggest { description: vec!["-v".to_string(), "file.txt".to_string()] } |
| ); |
| } |
| } |
|
|