Download crates/forge_main/src/model.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 58.4 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_main/src/model.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_main/src/model.rs
-
curl -L -o model.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_main/src/model.rs
58.4 kB
| 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; | |
| /// Top-level Clap parser used to dispatch slash/colon commands. | |
| /// | |
| /// The sentinel character (`/` or `:`) is stripped before passing tokens here, | |
| /// so Clap only sees the subcommand name and its arguments. | |
| struct ClapCmd { | |
| sub: AppCommand, | |
| } | |
| /// Result of agent command registration | |
| 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 | |
| } | |
| } | |
| pub struct ForgeCommand { | |
| pub name: String, | |
| pub description: String, | |
| pub value: Option<String>, | |
| } | |
| 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 { | |
| /// Sanitizes agent ID to create a valid command name | |
| /// Replaces spaces and special characters with hyphens | |
| 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("-") | |
| } | |
| /// Checks if a command name conflicts with built-in commands | |
| 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<_>>() | |
| } | |
| /// Registers workflow commands from the API. | |
| pub fn register_all(&self, commands: Vec<forge_domain::Command>) { | |
| let mut guard = self.commands.lock().unwrap(); | |
| // Remove existing workflow commands (those with ⚙ prefix in description) | |
| guard.retain(|cmd| !cmd.description.starts_with("⚙ ")); | |
| // Add new workflow commands | |
| 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); | |
| // Sort commands for consistent completion behavior | |
| guard.sort_by(|a, b| a.name.cmp(&b.name)); | |
| } | |
| /// Registers agent commands to the manager. | |
| /// Returns information about the registration process. | |
| 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() }; | |
| // Remove existing agent commands (commands starting with "agent-") | |
| guard.retain(|cmd| !cmd.name.starts_with("agent-")); | |
| // Add new agent commands | |
| 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}"); | |
| // Skip if it would conflict with reserved commands | |
| 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; | |
| } | |
| // Sort commands for consistent completion behavior | |
| guard.sort_by(|a, b| a.name.cmp(&b.name)); | |
| result | |
| } | |
| /// Finds a command by name. | |
| fn find(&self, command: &str) -> Option<ForgeCommand> { | |
| self.commands | |
| .lock() | |
| .unwrap() | |
| .iter() | |
| .find(|c| c.name == command) | |
| .cloned() | |
| } | |
| /// Lists all registered commands. | |
| pub fn list(&self) -> Vec<ForgeCommand> { | |
| self.commands.lock().unwrap().clone() | |
| } | |
| /// Extracts the command value from the input parts | |
| /// | |
| /// # Arguments | |
| /// * `command` - The command for which to extract the value | |
| /// * `parts` - The parts of the command input after the command name | |
| /// | |
| /// # Returns | |
| /// * `Option<String>` - The extracted value, if any | |
| fn extract_command_value(&self, command: &ForgeCommand, parts: &[&str]) -> Option<String> { | |
| // Unit tests implemented in the test module below | |
| // Try to get value provided in the command | |
| let value_provided = if !parts.is_empty() { | |
| Some(parts.join(" ")) | |
| } else { | |
| None | |
| }; | |
| // Try to get default value from command definition | |
| let value_default = self | |
| .commands | |
| .lock() | |
| .unwrap() | |
| .iter() | |
| .find(|c| c.name == command.name) | |
| .and_then(|cmd| cmd.value.clone()); | |
| // Use provided value if non-empty, otherwise use default | |
| match value_provided { | |
| Some(value) if !value.trim().is_empty() => Some(value), | |
| _ => value_default, | |
| } | |
| } | |
| pub fn parse(&self, input: &str) -> anyhow::Result<AppCommand> { | |
| // Shell commands (start with !) bypass Clap entirely. | |
| 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(""); | |
| // Non-command input — pass straight through as a message. | |
| let is_command = first.starts_with('/') || first.starts_with(':'); | |
| if !is_command { | |
| return Ok(AppCommand::Message(input.to_string())); | |
| } | |
| // Strip the sentinel character so Clap only sees the bare command name. | |
| 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(); | |
| // Build argv: [bare_command, arg1, arg2, …] | |
| 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) => { | |
| // Post-process variants that need Vec<String> → concrete type fixup | |
| 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) => { | |
| // Clap failed — check whether this is an agent command or a | |
| // registered custom workflow command before surfacing the error. | |
| let command_name = bare; | |
| // Give a domain-specific error for rename with no name argument. | |
| if (command_name == "rename" || command_name == "rn") && rest.is_empty() { | |
| return Err(anyhow::anyhow!( | |
| "Usage: :rename <name>. Please provide a name for the conversation." | |
| )); | |
| } | |
| // Check if it's an agent command pattern (agent-*) | |
| 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" | |
| )); | |
| } | |
| // Handle custom workflow commands | |
| 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, | |
| ))); | |
| } | |
| // Surface user-friendly errors for unknown commands. | |
| if clap_err.kind() == ErrorKind::InvalidSubcommand { | |
| return Err(anyhow::anyhow!( | |
| "Unknown command '{command_prefix}{command_name}'. Run '{command_prefix}help' to list available commands." | |
| )); | |
| } | |
| // Surface a clean error from Clap (strips ANSI + internal parser name). | |
| let rendered = clap_err.render().to_string(); | |
| let cleaned = rendered.replace("forge_cmd", "forge"); | |
| Err(anyhow::anyhow!("{}", cleaned.trim())) | |
| } | |
| } | |
| } | |
| } | |
| /// Represents user input types in the chat application. | |
| /// | |
| /// This enum encapsulates all forms of input including: | |
| /// - System commands (starting with '/') | |
| /// - Regular chat messages | |
| /// - File content | |
| pub enum AppCommand { | |
| /// Display the effective resolved configuration. | |
| /// This can be triggered with the '/config' command (aliases: env, e). | |
| )] | |
| Config, | |
| /// Set the global model via interactive selection. | |
| /// This can be triggered with the '/config-model' command (alias: cm). | |
| ConfigModel, | |
| /// Reset session overrides to global config. | |
| /// This can be triggered with the '/config-reload' command (aliases: cr, | |
| /// model-reset, mr). | |
| )] | |
| ConfigReload, | |
| /// Set the reasoning effort level. | |
| /// This can be triggered with the '/reasoning-effort' command (alias: re). | |
| ReasoningEffort, | |
| /// Set the reasoning effort level in global config. | |
| /// This can be triggered with the '/config-reasoning-effort' command | |
| /// (alias: cre). | |
| ConfigReasoningEffort, | |
| /// Set the model used for commit message generation. | |
| /// This can be triggered with the '/config-commit-model' command (alias: | |
| /// ccm). | |
| ConfigCommitModel, | |
| /// Set the model used for command suggestion generation. | |
| /// This can be triggered with the '/config-suggest-model' command (alias: | |
| /// csm). | |
| ConfigSuggestModel, | |
| /// Open the global config file in an editor. | |
| /// This can be triggered with the '/config-edit' command (alias: ce). | |
| ConfigEdit, | |
| /// List all available skills. | |
| /// This can be triggered with the '/skill' command. | |
| Skill, | |
| /// Open an external editor to write a prompt. | |
| /// This can be triggered with the '/edit' command (alias: ed). | |
| Edit { | |
| /// Initial content for the editor (optional) | |
| content: Vec<String>, | |
| }, | |
| /// Preview the AI-generated commit message without committing. | |
| /// This can be triggered with the '/commit-preview' command. | |
| CommitPreview, | |
| /// Generate a shell command from a natural language description. | |
| /// This can be triggered with the '/suggest' command (alias: s). | |
| Suggest { | |
| /// Natural language description of the shell command | |
| description: Vec<String>, | |
| }, | |
| /// Clone the current or a selected conversation. | |
| /// This can be triggered with the '/clone' command. | |
| Clone { | |
| /// Conversation ID to clone (optional — prompts interactively if | |
| /// absent) | |
| id: Option<String>, | |
| }, | |
| /// Rename any conversation interactively. | |
| /// This can be triggered with the '/conversation-rename' command. | |
| ConversationRename { | |
| /// New name for the conversation (optional — prompts interactively if | |
| /// absent) | |
| name: Vec<String>, | |
| }, | |
| /// Copy the last AI response to the clipboard. | |
| /// This can be triggered with the '/copy' command. | |
| Copy, | |
| /// Sync the current workspace for semantic search. | |
| /// This can be triggered with the '/workspace-sync' command (alias: sync). | |
| WorkspaceSync, | |
| /// Show sync status of all workspace files. | |
| /// This can be triggered with the '/workspace-status' command. | |
| WorkspaceStatus, | |
| /// Show workspace information with sync details. | |
| /// This can be triggered with the '/workspace-info' command. | |
| WorkspaceInfo, | |
| /// Initialize a new workspace without syncing files. | |
| /// This can be triggered with the '/workspace-init' command. | |
| WorkspaceInit, | |
| /// Compact the conversation context. This can be triggered with the | |
| /// '/compact' command. | |
| Compact, | |
| /// Start a new conversation while preserving history. | |
| /// This can be triggered with the '/new' command. | |
| New, | |
| /// A regular text message from the user to be processed by the chat system. | |
| /// Any input that doesn't start with '/' is treated as a message. | |
| Message(String), | |
| /// Display system environment information. | |
| /// This can be triggered with the '/info' command. | |
| Info, | |
| /// Display usage information (tokens & requests). | |
| Usage, | |
| /// Exit the application without any further action. | |
| Exit, | |
| /// Updates the forge version | |
| Update, | |
| /// Switch to "forge" agent. | |
| /// This can be triggered with the '/act' command (alias: forge). | |
| Forge, | |
| /// Switch to "muse" agent. | |
| /// This can be triggered with the '/plan' command (alias: muse). | |
| Muse, | |
| /// Switch to "sage" agent. | |
| /// This can be triggered with the '/sage' command. | |
| Sage, | |
| /// Switch to "help" mode. | |
| /// This can be triggered with the '/help' command. | |
| Help, | |
| /// Dumps the current conversation into a json file or html file | |
| Dump { | |
| /// Output as HTML instead of JSON | |
| html: bool, | |
| }, | |
| /// Switch or select the active model | |
| /// This can be triggered with the '/model' command. | |
| Model, | |
| /// List all available tools with their descriptions and schema | |
| /// This can be triggered with the '/tools' command. | |
| Tools, | |
| /// Handles custom command defined in workflow file. | |
| Custom(UserCommand), | |
| /// Executes a native shell command. | |
| /// This can be triggered with commands starting with '!' character. | |
| Shell(String), | |
| /// Allows user to switch the operating agent. | |
| Agent, | |
| /// Allows you to configure provider | |
| )] | |
| Login, | |
| /// Logs out from the configured provider | |
| Logout, | |
| /// Retry without modifying model context | |
| Retry, | |
| /// List all conversations for the active workspace | |
| )] | |
| Conversations { | |
| /// Conversation ID to switch to directly (optional — shows interactive | |
| /// picker if absent) | |
| id: Option<String>, | |
| }, | |
| /// Show nested conversations spawned by the current conversation | |
| ConversationTree, | |
| /// Delete a conversation permanently | |
| Delete, | |
| /// Rename the current conversation | |
| Rename { | |
| /// New name for the conversation | |
| name: Vec<String>, | |
| }, | |
| /// Switch directly to a specific agent by ID | |
| AgentSwitch(String), | |
| /// Generate and optionally commit changes with AI-generated message | |
| /// | |
| /// Examples: | |
| /// - `:commit` - Generate message and commit | |
| /// - `:commit 5000` - Commit with max diff of 5000 bytes | |
| Commit { | |
| /// Optional arguments (numeric value sets max diff size in bytes) | |
| args: Vec<String>, | |
| /// Parsed max diff size (set by parse() from args) | |
| max_diff_size: Option<usize>, | |
| }, | |
| /// Index the current workspace for semantic code 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", | |
| } | |
| } | |
| /// Returns the usage description for the command. | |
| pub fn usage(&self) -> &str { | |
| self.get_str("usage").unwrap() | |
| } | |
| /// Returns true for internal/meta variants that should not appear in the | |
| /// public `forge list commands` output or the REPL help listing. | |
| pub fn is_internal(&self) -> bool { | |
| matches!( | |
| self, | |
| AppCommand::Message(_) | |
| | AppCommand::Custom(_) | |
| | AppCommand::Shell(_) | |
| | AppCommand::AgentSwitch(_) | |
| | AppCommand::Rename { .. } | |
| ) | |
| } | |
| /// Returns true for variants that are pure agent-switch shorthands whose | |
| /// canonical name matches a built-in agent (forge, muse, sage). These | |
| /// commands are already emitted as AGENT rows by the agent-info loop in | |
| /// `on_show_commands`, so they must be excluded from the COMMAND loop to | |
| /// avoid duplicate entries in `list commands --porcelain`. | |
| pub fn is_agent_switch(&self) -> bool { | |
| matches!( | |
| self, | |
| AppCommand::Forge | AppCommand::Muse | AppCommand::Sage | |
| ) | |
| } | |
| } | |
| 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; | |
| /// Test-only wrapper for displaying models in selection menus | |
| 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(()) | |
| } | |
| } | |
| /// Test-only wrapper for displaying providers in selection menus | |
| 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(()) | |
| } | |
| } | |
| fn test_extract_command_value_with_provided_value() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| let command = ForgeCommand { | |
| name: String::from("/test"), | |
| description: String::from("Test command"), | |
| value: None, | |
| }; | |
| let parts = vec!["arg1", "arg2"]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify | |
| assert_eq!(result, Some(String::from("arg1 arg2"))); | |
| } | |
| fn test_extract_command_value_with_empty_parts_default_value() { | |
| // Setup | |
| 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![]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify | |
| assert_eq!(result, Some(String::from("default_value"))); | |
| } | |
| fn test_extract_command_value_with_empty_string_parts() { | |
| // Setup | |
| 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![""]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify - should use default as the provided value is empty | |
| assert_eq!(result, Some(String::from("default_value"))); | |
| } | |
| fn test_extract_command_value_with_whitespace_parts() { | |
| // Setup | |
| 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![" "]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify - should use default as the provided value is just whitespace | |
| assert_eq!(result, Some(String::from("default_value"))); | |
| } | |
| fn test_extract_command_value_no_default_no_provided() { | |
| // Setup | |
| 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![]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify - should be None as there's no default and no provided value | |
| assert_eq!(result, None); | |
| } | |
| fn test_extract_command_value_provided_overrides_default() { | |
| // Setup | |
| 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"]; | |
| // Execute | |
| let result = cmd_manager.extract_command_value(&command, &parts); | |
| // Verify - provided value should override default | |
| assert_eq!(result, Some(String::from("provided_value"))); | |
| } | |
| fn test_parse_shell_command() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse("!ls -la").unwrap(); | |
| // Verify | |
| match result { | |
| AppCommand::Shell(cmd) => assert_eq!(cmd, "ls -la"), | |
| _ => panic!("Expected Shell command, got {result:?}"), | |
| } | |
| } | |
| fn test_parse_shell_command_empty() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse("!").unwrap(); | |
| // Verify | |
| match result { | |
| AppCommand::Shell(cmd) => assert_eq!(cmd, ""), | |
| _ => panic!("Expected Shell command, got {result:?}"), | |
| } | |
| } | |
| fn test_parse_shell_command_with_whitespace() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse("! echo 'test' ").unwrap(); | |
| // Verify | |
| match result { | |
| AppCommand::Shell(cmd) => assert_eq!(cmd, "echo 'test'"), | |
| _ => panic!("Expected Shell command, got {result:?}"), | |
| } | |
| } | |
| fn test_shell_command_not_in_default_commands() { | |
| // Setup | |
| let manager = ForgeCommandManager::default(); | |
| let commands = manager.list(); | |
| // The shell command should not be included | |
| let contains_shell = commands.iter().any(|cmd| cmd.name == "!shell"); | |
| assert!( | |
| !contains_shell, | |
| "Shell command should not be in default commands" | |
| ); | |
| } | |
| fn test_parse_list_command() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse("/conversation").unwrap(); | |
| // Verify | |
| match result { | |
| AppCommand::Conversations { .. } => { | |
| // Command parsed correctly | |
| } | |
| _ => panic!("Expected List command, got {result:?}"), | |
| } | |
| } | |
| fn test_parse_conversation_with_id() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let actual = cmd_manager | |
| .parse("/conversation 550e8400-e29b-41d4-a716-446655440000") | |
| .unwrap(); | |
| // Verify | |
| let expected = AppCommand::Conversations { | |
| id: Some("550e8400-e29b-41d4-a716-446655440000".to_string()), | |
| }; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_list_command_in_default_commands() { | |
| // Setup | |
| let manager = ForgeCommandManager::default(); | |
| let commands = manager.list(); | |
| // The list command should be included | |
| let contains_list = commands.iter().any(|cmd| cmd.name == "conversation"); | |
| assert!( | |
| contains_list, | |
| "Conversations command should be in default commands" | |
| ); | |
| } | |
| fn test_sanitize_agent_id_basic() { | |
| // Test basic sanitization | |
| let fixture = "test-agent"; | |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); | |
| let expected = "test-agent"; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_sanitize_agent_id_with_spaces() { | |
| // Test space replacement | |
| let fixture = "test agent name"; | |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); | |
| let expected = "test-agent-name"; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_sanitize_agent_id_with_special_chars() { | |
| // Test special character replacement | |
| let fixture = "test@agent#name!"; | |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); | |
| let expected = "test-agent-name"; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_sanitize_agent_id_uppercase() { | |
| // Test uppercase conversion | |
| let fixture = "TestAgent"; | |
| let actual = ForgeCommandManager::sanitize_agent_id(fixture); | |
| let expected = "testagent"; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_is_reserved_command() { | |
| // Test reserved commands | |
| 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")); | |
| } | |
| fn test_register_agent_commands() { | |
| // Setup | |
| 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()), | |
| ]; | |
| // Execute | |
| let result = fixture.register_agent_commands(agents); | |
| // Verify result | |
| assert_eq!(result.registered_count, 2); | |
| assert_eq!(result.skipped_conflicts.len(), 0); | |
| // Verify | |
| 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")); | |
| } | |
| fn test_parse_agent_switch_command() { | |
| // Setup | |
| 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); | |
| // Execute | |
| let actual = fixture.parse("/agent-test-agent").unwrap(); | |
| // Verify | |
| 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], | |
| } | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| 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:?}"), | |
| } | |
| } | |
| 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:?}"), | |
| } | |
| } | |
| 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:?}"), | |
| } | |
| } | |
| 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:?}"), | |
| } | |
| } | |
| 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" | |
| ); | |
| } | |
| fn test_parse_invalid_agent_command() { | |
| // Setup | |
| let fixture = ForgeCommandManager::default(); | |
| // Execute | |
| let result = fixture.parse("/agent-nonexistent"); | |
| // Verify | |
| assert!(result.is_err()); | |
| assert!( | |
| result | |
| .unwrap_err() | |
| .to_string() | |
| .contains("not a valid agent command") | |
| ); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| fn test_parse_tool_command() { | |
| // Setup | |
| let fixture = ForgeCommandManager::default(); | |
| // Execute | |
| let result = fixture.parse("/tools").unwrap(); | |
| // Verify | |
| match result { | |
| AppCommand::Tools => { | |
| // Command parsed correctly | |
| } | |
| _ => panic!("Expected Tool command, got {result:?}"), | |
| } | |
| } | |
| fn test_parse_dump_command_json() { | |
| // Setup | |
| let fixture = ForgeCommandManager::default(); | |
| // Execute | |
| let actual = fixture.parse("/dump").unwrap(); | |
| // Verify | |
| let expected = AppCommand::Dump { html: false }; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_parse_dump_command_html_with_flag() { | |
| // Setup | |
| let fixture = ForgeCommandManager::default(); | |
| // Execute | |
| let actual = fixture.parse("/dump --html").unwrap(); | |
| // Verify | |
| let expected = AppCommand::Dump { html: true }; | |
| assert_eq!(actual, expected); | |
| } | |
| 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()] } | |
| ); | |
| } | |
| 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() | |
| ] | |
| } | |
| ); | |
| } | |
| 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")); | |
| } | |
| 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()] } | |
| ); | |
| } | |
| 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()] } | |
| ); | |
| } | |
| fn test_rename_is_reserved_command() { | |
| assert!(ForgeCommandManager::is_reserved_command("rename")); | |
| assert!(ForgeCommandManager::is_reserved_command("rn")); | |
| } | |
| fn test_rename_command_name() { | |
| let cmd = AppCommand::Rename { name: vec!["test".to_string()] }; | |
| assert_eq!(cmd.name(), "rename"); | |
| } | |
| fn test_parse_suggest_with_dash_prefixed_tokens() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse(":suggest --- date").unwrap(); | |
| // Verify | |
| assert_eq!( | |
| result, | |
| AppCommand::Suggest { description: vec!["---".to_string(), "date".to_string()] } | |
| ); | |
| } | |
| fn test_parse_suggest_with_double_dash_flags() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse(":suggest --date tomorrow").unwrap(); | |
| // Verify | |
| assert_eq!( | |
| result, | |
| AppCommand::Suggest { | |
| description: vec!["--date".to_string(), "tomorrow".to_string()] | |
| } | |
| ); | |
| } | |
| fn test_parse_suggest_with_single_dash() { | |
| // Setup | |
| let cmd_manager = ForgeCommandManager::default(); | |
| // Execute | |
| let result = cmd_manager.parse(":suggest -v file.txt").unwrap(); | |
| // Verify | |
| assert_eq!( | |
| result, | |
| AppCommand::Suggest { description: vec!["-v".to_string(), "file.txt".to_string()] } | |
| ); | |
| } | |
| } | |